From be152c153122db7e286de53a6df41414b30a2130 Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Sun, 3 Aug 2025 21:26:50 +0530 Subject: [PATCH 1/3] Fix: Migrations --- .DS_Store | Bin 6148 -> 8196 bytes DynamicIsland.xcodeproj/project.pbxproj | 56 ++ .../project.pbxproj.backup | 533 +++++++++++++++ DynamicIsland/DynamicIslandApp.swift | 6 +- .../DynamicIslandViewCoordinator.swift | 107 +-- DynamicIsland/Localizable.xcstrings | 163 +---- .../AppleMusicController.swift | 179 ++--- .../MediaControllerProtocol.swift | 21 +- .../NowPlayingController.swift | 313 +++++---- .../MediaControllers/SpotifyController.swift | 135 ++-- .../YouTubeMusicController.swift | 486 +++++++------ .../Calendar/DynamicIslandCalendar.swift | 2 +- .../components/Music/MusicVisualizer.swift | 4 +- .../Notch/DynamicIslandHeader.swift | 6 +- .../components/Notch/NotchHomeView.swift | 46 +- .../components/Settings/SettingsView.swift | 646 +++++------------- DynamicIsland/helpers/AppIcons.swift | 7 - DynamicIsland/helpers/MediaChecker.swift | 67 ++ DynamicIsland/managers/CalendarManager.swift | 22 +- DynamicIsland/managers/MusicManager.swift | 111 ++- .../managers/NotchSpaceManager.swift | 2 +- .../models/BatteryStatusViewModel.swift | 135 ++-- DynamicIsland/models/Constants.swift | 1 + .../models/DynamicIslandViewModel.swift | 46 ++ DynamicIsland/models/PlaybackState.swift | 10 +- .../observers/FullscreenMediaDetection.swift | 25 +- DynamicIsland/sizing/matters.swift | 6 +- .../MediaRemoteAdapter | 1 + .../MediaRemoteAdapter.framework/Resources | 1 + .../Versions/A/MediaRemoteAdapter | Bin 0 -> 462128 bytes .../Versions/A/Resources/Info.plist | 28 + .../Versions/A/_CodeSignature/CodeResources | 128 ++++ .../Versions/Current | 1 + .../MediaRemoteAdapter | 1 + .../MediaRemoteAdapter.framework/Resources | 1 + .../Versions/A/MediaRemoteAdapter | Bin 0 -> 462128 bytes .../Versions/A/Resources/Info.plist | 28 + .../Versions/A/_CodeSignature/CodeResources | 128 ++++ .../Versions/Current | 1 + mediaremote-adapter/NowPlayingTestClient | Bin 0 -> 77848 bytes mediaremote-adapter/mediaremote-adapter.pl | 257 +++++++ 41 files changed, 2381 insertions(+), 1329 deletions(-) create mode 100644 DynamicIsland.xcodeproj/project.pbxproj.backup create mode 100644 DynamicIsland/helpers/MediaChecker.swift create mode 120000 Frameworks/MediaRemoteAdapter.framework/MediaRemoteAdapter create mode 120000 Frameworks/MediaRemoteAdapter.framework/Resources create mode 100755 Frameworks/MediaRemoteAdapter.framework/Versions/A/MediaRemoteAdapter create mode 100644 Frameworks/MediaRemoteAdapter.framework/Versions/A/Resources/Info.plist create mode 100644 Frameworks/MediaRemoteAdapter.framework/Versions/A/_CodeSignature/CodeResources create mode 120000 Frameworks/MediaRemoteAdapter.framework/Versions/Current create mode 120000 mediaremote-adapter/MediaRemoteAdapter.framework/MediaRemoteAdapter create mode 120000 mediaremote-adapter/MediaRemoteAdapter.framework/Resources create mode 100755 mediaremote-adapter/MediaRemoteAdapter.framework/Versions/A/MediaRemoteAdapter create mode 100644 mediaremote-adapter/MediaRemoteAdapter.framework/Versions/A/Resources/Info.plist create mode 100644 mediaremote-adapter/MediaRemoteAdapter.framework/Versions/A/_CodeSignature/CodeResources create mode 120000 mediaremote-adapter/MediaRemoteAdapter.framework/Versions/Current create mode 100755 mediaremote-adapter/NowPlayingTestClient create mode 100755 mediaremote-adapter/mediaremote-adapter.pl diff --git a/.DS_Store b/.DS_Store index de5a495f7c15999e69f68673a23c017697e5c764..aae1f70a79be15487c55807c31cb5b466efa4277 100644 GIT binary patch literal 8196 zcmeHMZEn**5PcgW5mZG2RaJvfg)fju+`xq7M|UdGGsPYfS-D;J;Kro(~bLU>q>_ zsE-adngS5(ysU-C%mdUW3K$2>J*p`=efHpjsS7EF^XBZ2WH>Pnn0xf*aNZm)cy=K} zaq69Rkq(E8Jz8rDm;%cRNbcUkGi1!%$EEx|<@p)^Iyh$EAx?SzUdO!~OtaC^G&`a# zZ@2nmV&{hzb+EJG>+x&A8D^N${^JdpK3A9?qRcR440BwPb)?a4j_rGeaTf_ANp(KL z($)wizLY&})-h+AG1dutj>|K}*9zOC4Qw-2RpJ%xIyj*gV@%4dZm+_+{Y%z$ET1YY zO=?%1>3}N{=2`Lo?dF!3!+AQ*(g`i(LA<-(2WR_%`ev%A=#b&0?6)Q?#eMUeY?WB< z;yDJ~d423-h<@1@p3>Ks!X9lgrwScfD7|wDV N0xE+wrodlS;2NWFBL@Hg delta 144 zcmZp1XfcprU|?W$DortDU=RQ@Ie-{Mvv5r;6q~50D9Q&jfKmZ zSwS-5lNSieZk7~eW1Ku)NPlvLQ1|4&!kHV3H!?0}=im@z1}Xsp0d64S3NmzK;dkcA W{4$;(D;Ss{)`48WusNP*4l@AwBN+Ao diff --git a/DynamicIsland.xcodeproj/project.pbxproj b/DynamicIsland.xcodeproj/project.pbxproj index 90b5436..0c1810f 100644 --- a/DynamicIsland.xcodeproj/project.pbxproj +++ b/DynamicIsland.xcodeproj/project.pbxproj @@ -16,9 +16,16 @@ 7E61181A2E24071E0050006C /* SwiftUIIntrospect in Frameworks */ = {isa = PBXBuildFile; productRef = B1628B912CC260C0003D8DF3 /* SwiftUIIntrospect */; }; 7E61181B2E24071E0050006C /* LottieUI in Frameworks */ = {isa = PBXBuildFile; productRef = B194240A2CD10017003E5DC2 /* LottieUI */; }; 7E61181C2E24071E0050006C /* MacroVisionKit in Frameworks */ = {isa = PBXBuildFile; productRef = 146BC4362D366E9B0047C66A /* MacroVisionKit */; }; + 7E8130402E3FB40500E08F3F /* mediaremote-adapter.pl in Resources */ = {isa = PBXBuildFile; fileRef = 7E81303D2E3FB40500E08F3F /* mediaremote-adapter.pl */; }; + 7E8130412E3FB40500E08F3F /* NowPlayingTestClient in Resources */ = {isa = PBXBuildFile; fileRef = 7E81303E2E3FB40500E08F3F /* NowPlayingTestClient */; }; + BA90505C953246D0AB206451 /* MediaRemoteAdapter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F4C1DD09EC134302938E84F6 /* MediaRemoteAdapter.framework */; }; + C61F69420FB64DA98516CFBC /* MediaRemoteAdapter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = F4C1DD09EC134302938E84F6 /* MediaRemoteAdapter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 7E81303D2E3FB40500E08F3F /* mediaremote-adapter.pl */ = {isa = PBXFileReference; lastKnownFileType = text.script.perl; path = "mediaremote-adapter.pl"; sourceTree = ""; }; + 7E81303E2E3FB40500E08F3F /* NowPlayingTestClient */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; path = NowPlayingTestClient; sourceTree = ""; }; + F4C1DD09EC134302938E84F6 /* MediaRemoteAdapter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MediaRemoteAdapter.framework; path = "Frameworks/MediaRemoteAdapter.framework"; sourceTree = SOURCE_ROOT; }; F54406E72CD4987A00C7AFD9 /* DynamicIsland.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DynamicIsland.app; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -35,6 +42,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + BA90505C953246D0AB206451 /* MediaRemoteAdapter.framework in Frameworks */, 7E61181C2E24071E0050006C /* MacroVisionKit in Frameworks */, 7E61181B2E24071E0050006C /* LottieUI in Frameworks */, 7E61181A2E24071E0050006C /* SwiftUIIntrospect in Frameworks */, @@ -49,10 +57,43 @@ }; /* End PBXFrameworksBuildPhase section */ +/* Begin PBXCopyFilesBuildPhase section */ + B933CDB48C2F42C2887A9836 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 12; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + C61F69420FB64DA98516CFBC /* MediaRemoteAdapter.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXGroup section */ + 7E81303F2E3FB40500E08F3F /* mediaremote-adapter */ = { + isa = PBXGroup; + children = ( + 7E81303D2E3FB40500E08F3F /* mediaremote-adapter.pl */, + 7E81303E2E3FB40500E08F3F /* NowPlayingTestClient */, + F4C1DD09EC134302938E84F6 /* MediaRemoteAdapter.framework */, + ); + path = "mediaremote-adapter"; + sourceTree = ""; + }; + 7E81304F2E3FB42700E08F3F /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; F54406DE2CD4987A00C7AFD9 = { isa = PBXGroup; children = ( + 7E81304F2E3FB42700E08F3F /* Frameworks */, + 7E81303F2E3FB40500E08F3F /* mediaremote-adapter */, F54406E92CD4987A00C7AFD9 /* DynamicIsland */, F54406E82CD4987A00C7AFD9 /* Products */, ); @@ -76,6 +117,7 @@ F54406E32CD4987A00C7AFD9 /* Sources */, F54406E42CD4987A00C7AFD9 /* Frameworks */, F54406E52CD4987A00C7AFD9 /* Resources */, + B933CDB48C2F42C2887A9836 /* Embed Frameworks */, ); buildRules = ( ); @@ -151,6 +193,8 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 7E8130402E3FB40500E08F3F /* mediaremote-adapter.pl in Resources */, + 7E8130412E3FB40500E08F3F /* NowPlayingTestClient in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -296,6 +340,12 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"DynamicIsland/Preview Content\""; ENABLE_PREVIEWS = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)", + "$(PROJECT_DIR)/Frameworks", + "$(PROJECT_DIR)/mediaremote-adapter", + ); GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSAppleEventsUsageDescription = "Need for Spotify and Apple Music"; INFOPLIST_KEY_NSCameraUsageDescription = "We need your camera for Mirror mode"; @@ -323,6 +373,12 @@ CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"DynamicIsland/Preview Content\""; ENABLE_PREVIEWS = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)", + "$(PROJECT_DIR)/Frameworks", + "$(PROJECT_DIR)/mediaremote-adapter", + ); GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSAppleEventsUsageDescription = "Need for Spotify and Apple Music"; INFOPLIST_KEY_NSCameraUsageDescription = "We need your camera for Mirror mode"; diff --git a/DynamicIsland.xcodeproj/project.pbxproj.backup b/DynamicIsland.xcodeproj/project.pbxproj.backup new file mode 100644 index 0000000..afefed5 --- /dev/null +++ b/DynamicIsland.xcodeproj/project.pbxproj.backup @@ -0,0 +1,533 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 7E6118142E24071E0050006C /* LaunchAtLogin in Frameworks */ = {isa = PBXBuildFile; productRef = 14D032192C68F32E0096E6A1 /* LaunchAtLogin */; }; + 7E6118152E24071E0050006C /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 14D0321C2C68F3350096E6A1 /* Sparkle */; }; + 7E6118162E24071E0050006C /* KeyboardShortcuts in Frameworks */ = {isa = PBXBuildFile; productRef = B18654382C6F4990000B926A /* KeyboardShortcuts */; }; + 7E6118172E24071E0050006C /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = 9A987A0F2C73CA8D005CA465 /* Collections */; }; + 7E6118182E24071E0050006C /* TheBoringWorkerNotifier in Frameworks */ = {isa = PBXBuildFile; productRef = 1485442E2C8F049C00943381 /* TheBoringWorkerNotifier */; }; + 7E6118192E24071E0050006C /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = B19016212CC15B3D00E3F12E /* Defaults */; }; + 7E61181A2E24071E0050006C /* SwiftUIIntrospect in Frameworks */ = {isa = PBXBuildFile; productRef = B1628B912CC260C0003D8DF3 /* SwiftUIIntrospect */; }; + 7E61181B2E24071E0050006C /* LottieUI in Frameworks */ = {isa = PBXBuildFile; productRef = B194240A2CD10017003E5DC2 /* LottieUI */; }; + 7E61181C2E24071E0050006C /* MacroVisionKit in Frameworks */ = {isa = PBXBuildFile; productRef = 146BC4362D366E9B0047C66A /* MacroVisionKit */; }; + 7E8130402E3FB40500E08F3F /* mediaremote-adapter.pl in Resources */ = {isa = PBXBuildFile; fileRef = 7E81303D2E3FB40500E08F3F /* mediaremote-adapter.pl */; }; + 7E8130412E3FB40500E08F3F /* NowPlayingTestClient in Resources */ = {isa = PBXBuildFile; fileRef = 7E81303E2E3FB40500E08F3F /* NowPlayingTestClient */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 7E81303D2E3FB40500E08F3F /* mediaremote-adapter.pl */ = {isa = PBXFileReference; lastKnownFileType = text.script.perl; path = "mediaremote-adapter.pl"; sourceTree = ""; }; + 7E81303E2E3FB40500E08F3F /* NowPlayingTestClient */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; path = NowPlayingTestClient; sourceTree = ""; }; + F54406E72CD4987A00C7AFD9 /* DynamicIsland.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DynamicIsland.app; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + F54406E92CD4987A00C7AFD9 /* DynamicIsland */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = DynamicIsland; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + F54406E42CD4987A00C7AFD9 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7E61181C2E24071E0050006C /* MacroVisionKit in Frameworks */, + 7E61181B2E24071E0050006C /* LottieUI in Frameworks */, + 7E61181A2E24071E0050006C /* SwiftUIIntrospect in Frameworks */, + 7E6118192E24071E0050006C /* Defaults in Frameworks */, + 7E6118182E24071E0050006C /* TheBoringWorkerNotifier in Frameworks */, + 7E6118172E24071E0050006C /* Collections in Frameworks */, + 7E6118162E24071E0050006C /* KeyboardShortcuts in Frameworks */, + 7E6118152E24071E0050006C /* Sparkle in Frameworks */, + 7E6118142E24071E0050006C /* LaunchAtLogin in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 7E81303F2E3FB40500E08F3F /* mediaremote-adapter */ = { + isa = PBXGroup; + children = ( + 7E81303D2E3FB40500E08F3F /* mediaremote-adapter.pl */, + 7E81303E2E3FB40500E08F3F /* NowPlayingTestClient */, + ); + path = "mediaremote-adapter"; + sourceTree = ""; + }; + 7E81304F2E3FB42700E08F3F /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; + F54406DE2CD4987A00C7AFD9 = { + isa = PBXGroup; + children = ( + 7E81304F2E3FB42700E08F3F /* Frameworks */, + 7E81303F2E3FB40500E08F3F /* mediaremote-adapter */, + F54406E92CD4987A00C7AFD9 /* DynamicIsland */, + F54406E82CD4987A00C7AFD9 /* Products */, + ); + sourceTree = ""; + }; + F54406E82CD4987A00C7AFD9 /* Products */ = { + isa = PBXGroup; + children = ( + F54406E72CD4987A00C7AFD9 /* DynamicIsland.app */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + F54406E62CD4987A00C7AFD9 /* DynamicIsland */ = { + isa = PBXNativeTarget; + buildConfigurationList = F54406F62CD4987B00C7AFD9 /* Build configuration list for PBXNativeTarget "DynamicIsland" */; + buildPhases = ( + F54406E32CD4987A00C7AFD9 /* Sources */, + F54406E42CD4987A00C7AFD9 /* Frameworks */, + F54406E52CD4987A00C7AFD9 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + F54406E92CD4987A00C7AFD9 /* DynamicIsland */, + ); + name = DynamicIsland; + packageProductDependencies = ( + 14D032192C68F32E0096E6A1 /* LaunchAtLogin */, + 14D0321C2C68F3350096E6A1 /* Sparkle */, + B18654382C6F4990000B926A /* KeyboardShortcuts */, + 9A987A0F2C73CA8D005CA465 /* Collections */, + 1485442E2C8F049C00943381 /* TheBoringWorkerNotifier */, + B19016212CC15B3D00E3F12E /* Defaults */, + B1628B912CC260C0003D8DF3 /* SwiftUIIntrospect */, + B194240A2CD10017003E5DC2 /* LottieUI */, + 146BC4362D366E9B0047C66A /* MacroVisionKit */, + ); + productName = DynamicIsland; + productReference = F54406E72CD4987A00C7AFD9 /* DynamicIsland.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + F54406DF2CD4987A00C7AFD9 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1600; + LastUpgradeCheck = 1600; + TargetAttributes = { + F54406E62CD4987A00C7AFD9 = { + CreatedOnToolsVersion = 16.0; + }; + }; + }; + buildConfigurationList = F54406E22CD4987A00C7AFD9 /* Build configuration list for PBXProject "DynamicIsland" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = F54406DE2CD4987A00C7AFD9; + minimizedProjectReferenceProxies = 1; + packageReferences = ( + F54407082CD4A23F00C7AFD9 /* XCRemoteSwiftPackageReference "swift-collections" */, + 14D032182C68F32E0096E6A1 /* XCRemoteSwiftPackageReference "LaunchAtLogin-Modern" */, + 14D0321B2C68F3350096E6A1 /* XCRemoteSwiftPackageReference "Sparkle" */, + B18654372C6F4990000B926A /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */, + 9A987A112C73CAA1005CA465 /* XCRemoteSwiftPackageReference "Pow" */, + 1485442D2C8F049C00943381 /* XCRemoteSwiftPackageReference "TheBoringWorkerNotifier" */, + B19016202CC15B3D00E3F12E /* XCRemoteSwiftPackageReference "Defaults" */, + B1628B902CC260C0003D8DF3 /* XCRemoteSwiftPackageReference "swiftui-introspect" */, + B19424072CD0DB52003E5DC2 /* XCRemoteSwiftPackageReference "LottieUI" */, + 146BC4352D366E9B0047C66A /* XCRemoteSwiftPackageReference "MacroVisionKit" */, + ); + preferredProjectObjectVersion = 77; + productRefGroup = F54406E82CD4987A00C7AFD9 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + F54406E62CD4987A00C7AFD9 /* DynamicIsland */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + F54406E52CD4987A00C7AFD9 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7E8130402E3FB40500E08F3F /* mediaremote-adapter.pl in Resources */, + 7E8130412E3FB40500E08F3F /* NowPlayingTestClient in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + F54406E32CD4987A00C7AFD9 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + F54406F42CD4987B00C7AFD9 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 15.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + F54406F52CD4987B00C7AFD9 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 15.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + }; + name = Release; + }; + F54406F72CD4987B00C7AFD9 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = DynamicIsland/DynamicIsland.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"DynamicIsland/Preview Content\""; + ENABLE_PREVIEWS = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)", + "$(SDKROOT)/../../../../../../../../../Users/hariharan/HobbyProjects/DynamicIsland", + "$(PROJECT_DIR)/mediaremote-adapter\\ 2", + ); + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSAppleEventsUsageDescription = "Need for Spotify and Apple Music"; + INFOPLIST_KEY_NSCameraUsageDescription = "We need your camera for Mirror mode"; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.karthikinformationtechnology.DynamicIsland; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + F54406F82CD4987B00C7AFD9 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = DynamicIsland/DynamicIsland.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"DynamicIsland/Preview Content\""; + ENABLE_PREVIEWS = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)", + "$(SDKROOT)/../../../../../../../../../Users/hariharan/HobbyProjects/DynamicIsland", + "$(PROJECT_DIR)/mediaremote-adapter\\ 2", + ); + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSAppleEventsUsageDescription = "Need for Spotify and Apple Music"; + INFOPLIST_KEY_NSCameraUsageDescription = "We need your camera for Mirror mode"; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.karthikinformationtechnology.DynamicIsland; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + F54406E22CD4987A00C7AFD9 /* Build configuration list for PBXProject "DynamicIsland" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F54406F42CD4987B00C7AFD9 /* Debug */, + F54406F52CD4987B00C7AFD9 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F54406F62CD4987B00C7AFD9 /* Build configuration list for PBXNativeTarget "DynamicIsland" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F54406F72CD4987B00C7AFD9 /* Debug */, + F54406F82CD4987B00C7AFD9 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 146BC4352D366E9B0047C66A /* XCRemoteSwiftPackageReference "MacroVisionKit" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/TheBoredTeam/MacroVisionKit"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.1.0; + }; + }; + 1485442D2C8F049C00943381 /* XCRemoteSwiftPackageReference "TheBoringWorkerNotifier" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/TheBoredTeam/TheBoringWorkerNotifier.git"; + requirement = { + branch = main; + kind = branch; + }; + }; + 14D032182C68F32E0096E6A1 /* XCRemoteSwiftPackageReference "LaunchAtLogin-Modern" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/sindresorhus/LaunchAtLogin-Modern"; + requirement = { + branch = main; + kind = branch; + }; + }; + 14D0321B2C68F3350096E6A1 /* XCRemoteSwiftPackageReference "Sparkle" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/sparkle-project/Sparkle"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.6.4; + }; + }; + 9A987A112C73CAA1005CA465 /* XCRemoteSwiftPackageReference "Pow" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/EmergeTools/Pow"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.0.4; + }; + }; + B1628B902CC260C0003D8DF3 /* XCRemoteSwiftPackageReference "swiftui-introspect" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/siteline/swiftui-introspect"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.3.0; + }; + }; + B18654372C6F4990000B926A /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/sindresorhus/KeyboardShortcuts"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.2.4; + }; + }; + B19016202CC15B3D00E3F12E /* XCRemoteSwiftPackageReference "Defaults" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/sindresorhus/Defaults"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 9.0.2; + }; + }; + B19424072CD0DB52003E5DC2 /* XCRemoteSwiftPackageReference "LottieUI" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/jasudev/LottieUI.git"; + requirement = { + branch = main; + kind = branch; + }; + }; + F54407082CD4A23F00C7AFD9 /* XCRemoteSwiftPackageReference "swift-collections" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/apple/swift-collections.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.1.4; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 146BC4362D366E9B0047C66A /* MacroVisionKit */ = { + isa = XCSwiftPackageProductDependency; + package = 146BC4352D366E9B0047C66A /* XCRemoteSwiftPackageReference "MacroVisionKit" */; + productName = MacroVisionKit; + }; + 1485442E2C8F049C00943381 /* TheBoringWorkerNotifier */ = { + isa = XCSwiftPackageProductDependency; + package = 1485442D2C8F049C00943381 /* XCRemoteSwiftPackageReference "TheBoringWorkerNotifier" */; + productName = TheBoringWorkerNotifier; + }; + 14D032192C68F32E0096E6A1 /* LaunchAtLogin */ = { + isa = XCSwiftPackageProductDependency; + package = 14D032182C68F32E0096E6A1 /* XCRemoteSwiftPackageReference "LaunchAtLogin-Modern" */; + productName = LaunchAtLogin; + }; + 14D0321C2C68F3350096E6A1 /* Sparkle */ = { + isa = XCSwiftPackageProductDependency; + package = 14D0321B2C68F3350096E6A1 /* XCRemoteSwiftPackageReference "Sparkle" */; + productName = Sparkle; + }; + 9A987A0F2C73CA8D005CA465 /* Collections */ = { + isa = XCSwiftPackageProductDependency; + package = F54407082CD4A23F00C7AFD9 /* XCRemoteSwiftPackageReference "swift-collections" */; + productName = Collections; + }; + B1628B912CC260C0003D8DF3 /* SwiftUIIntrospect */ = { + isa = XCSwiftPackageProductDependency; + package = B1628B902CC260C0003D8DF3 /* XCRemoteSwiftPackageReference "swiftui-introspect" */; + productName = SwiftUIIntrospect; + }; + B18654382C6F4990000B926A /* KeyboardShortcuts */ = { + isa = XCSwiftPackageProductDependency; + package = B18654372C6F4990000B926A /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */; + productName = KeyboardShortcuts; + }; + B19016212CC15B3D00E3F12E /* Defaults */ = { + isa = XCSwiftPackageProductDependency; + package = B19016202CC15B3D00E3F12E /* XCRemoteSwiftPackageReference "Defaults" */; + productName = Defaults; + }; + B194240A2CD10017003E5DC2 /* LottieUI */ = { + isa = XCSwiftPackageProductDependency; + package = B19424072CD0DB52003E5DC2 /* XCRemoteSwiftPackageReference "LottieUI" */; + productName = LottieUI; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = F54406DF2CD4987A00C7AFD9 /* Project object */; +} diff --git a/DynamicIsland/DynamicIslandApp.swift b/DynamicIsland/DynamicIslandApp.swift index 102ff7a..72897f9 100644 --- a/DynamicIsland/DynamicIslandApp.swift +++ b/DynamicIsland/DynamicIslandApp.swift @@ -55,10 +55,10 @@ struct DynamicNotchApp: App { workspace.openApplication(at: appURL, configuration: configuration) } - NSApplication.shared.terminate(nil) + NSApplication.shared.terminate(self) } Button("Quit", role: .destructive) { - NSApp.terminate(nil) + NSApplication.shared.terminate(self) } .keyboardShortcut(KeyEquivalent("Q"), modifiers: .command) } @@ -80,7 +80,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { @ObservedObject var coordinator = DynamicIslandViewCoordinator.shared var whatsNewWindow: NSWindow? var timer: Timer? - let calendarManager = CalendarManager() + let calendarManager = CalendarManager.shared let webcamManager = WebcamManager.shared var closeNotchWorkItem: DispatchWorkItem? private var previousScreens: [NSScreen]? diff --git a/DynamicIsland/DynamicIslandViewCoordinator.swift b/DynamicIsland/DynamicIslandViewCoordinator.swift index af545a7..23c4553 100644 --- a/DynamicIsland/DynamicIslandViewCoordinator.swift +++ b/DynamicIsland/DynamicIslandViewCoordinator.swift @@ -52,8 +52,6 @@ class DynamicIslandViewCoordinator: ObservableObject { var notifier: TheBoringWorkerNotifier = .init() @Published var currentView: NotchViews = .home - private var sneakPeekDispatch: DispatchWorkItem? - private var expandingViewDispatch: DispatchWorkItem? @AppStorage("firstLaunch") var firstLaunch: Bool = true @@ -109,21 +107,35 @@ class DynamicIslandViewCoordinator: ObservableObject { } @objc func sneakPeekEvent(_ notification: Notification) { - let decoder = JSONDecoder() - if let decodedData = try? decoder.decode(SharedSneakPeek.self, from: notification.userInfo?.first?.value as! Data) { - let contentType = decodedData.type == "brightness" ? SneakContentType.brightness : decodedData.type == "volume" ? SneakContentType.volume : decodedData.type == "backlight" ? SneakContentType.backlight : decodedData.type == "mic" ? SneakContentType.mic : decodedData.type == "timer" ? SneakContentType.timer : SneakContentType.brightness + let decoder = JSONDecoder() + if let decodedData = try? decoder.decode( + SharedSneakPeek.self, from: notification.userInfo?.first?.value as! Data) + { + let contentType = + decodedData.type == "brightness" + ? SneakContentType.brightness + : decodedData.type == "volume" + ? SneakContentType.volume + : decodedData.type == "backlight" + ? SneakContentType.backlight + : decodedData.type == "mic" + ? SneakContentType.mic + : decodedData.type == "timer" + ? SneakContentType.timer : SneakContentType.brightness + + let formatter = NumberFormatter() + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.numberStyle = .decimal + let value = CGFloat((formatter.number(from: decodedData.value) ?? 0.0).floatValue) + let icon = decodedData.icon - let value = CGFloat((NumberFormatter().number(from: decodedData.value) ?? 0.0).floatValue) - let icon = decodedData.icon - - print(decodedData) - - toggleSneakPeek(status: decodedData.show, type: contentType, value: value, icon: icon) - - } else { - print("Failed to decode JSON data") - } + print("Decoded: \(decodedData), Parsed value: \(value)") + + toggleSneakPeek(status: decodedData.show, type: contentType, value: value, icon: icon) + } else { + print("Failed to decode JSON data") } + } func toggleSneakPeek(status: Bool, type: SneakContentType, duration: TimeInterval = 1.5, value: CGFloat = 0, icon: String = "") { sneakPeekDuration = duration @@ -148,35 +160,41 @@ class DynamicIslandViewCoordinator: ObservableObject { } private var sneakPeekDuration: TimeInterval = 1.5 - - @Published var sneakPeek: sneakPeek = .init() { - didSet { - if sneakPeek.show { - sneakPeekDispatch?.cancel() + private var sneakPeekTask: Task? - sneakPeekDispatch = DispatchWorkItem { [weak self] in - guard let self = self else { return } - withAnimation { - self.toggleSneakPeek(status: false, type: SneakContentType.music) - self.sneakPeekDuration = 1.5 - } + // Helper function to manage sneakPeek timer using Swift Concurrency + private func scheduleSneakPeekHide(after duration: TimeInterval) { + sneakPeekTask?.cancel() + + sneakPeekTask = Task { [weak self] in + try? await Task.sleep(for: .seconds(duration)) + guard let self = self, !Task.isCancelled else { return } + await MainActor.run { + withAnimation { + self.toggleSneakPeek(status: false, type: .music) + self.sneakPeekDuration = 1.5 } - DispatchQueue.main - .asyncAfter( - deadline: .now() + self.sneakPeekDuration, - execute: sneakPeekDispatch! - ) } } } - func toggleExpandingView(status: Bool, type: SneakContentType, value: CGFloat = 0, browser: BrowserType = .chromium) { - if expandingView.show { - withAnimation(.smooth) { - self.expandingView.show = false + @Published var sneakPeek: sneakPeek = .init() { + didSet { + if sneakPeek.show { + scheduleSneakPeekHide(after: sneakPeekDuration) + } else { + sneakPeekTask?.cancel() } } - DispatchQueue.main.async { + } + + func toggleExpandingView( + status: Bool, + type: SneakContentType, + value: CGFloat = 0, + browser: BrowserType = .chromium + ) { + Task { @MainActor in withAnimation(.smooth) { self.expandingView.show = status self.expandingView.type = type @@ -185,18 +203,21 @@ class DynamicIslandViewCoordinator: ObservableObject { } } } + + private var expandingViewTask: Task? @Published var expandingView: ExpandedItem = .init() { didSet { if expandingView.show { - expandingViewDispatch?.cancel() - - expandingViewDispatch = DispatchWorkItem { [weak self] in - guard let self = self else { return } - self.toggleExpandingView(status: false, type: SneakContentType.battery) + expandingViewTask?.cancel() + let duration: TimeInterval = (expandingView.type == .download ? 2 : 3) + expandingViewTask = Task { [weak self] in + try? await Task.sleep(for: .seconds(duration)) + guard let self = self, !Task.isCancelled else { return } + self.toggleExpandingView(status: false, type: .battery) } - - DispatchQueue.main.asyncAfter(deadline: .now() + (expandingView.type == .download ? 2 : 3), execute: expandingViewDispatch!) + } else { + expandingViewTask?.cancel() } } } diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings index a142c91..aa367ab 100644 --- a/DynamicIsland/Localizable.xcstrings +++ b/DynamicIsland/Localizable.xcstrings @@ -34,6 +34,9 @@ }, "%lld" : { + }, + "%lld items" : { + }, "%lld%%" : { @@ -49,18 +52,6 @@ }, "• New feature 1" : { - }, - "3 items" : { - - }, - "5 items" : { - - }, - "7 items" : { - - }, - "10 items" : { - }, "About" : { "localizations" : { @@ -85,9 +76,6 @@ }, "Access Denied" : { - }, - "Actions" : { - }, "Activate" : { @@ -253,14 +241,11 @@ }, "Check for Updates…" : { - }, - "Choose a custom sound file that will play when the timer completes. Supported formats: MP3, M4A, WAV, AIFF." : { - }, "Choose a Music Source" : { }, - "Choose File" : { + "Choose which performance graphs to display. 1-3 graphs use horizontal layout, 4+ graphs use compressed grid layout." : { }, "Circle" : { @@ -268,18 +253,15 @@ }, "Clear" : { - }, - "Clear Clipboard History" : { - - }, - "Clear Data" : { - }, "Clipboard" : { }, "Clipboard History" : { + }, + "Clipboard history will be tracked and accessible via the header button." : { + }, "Clipboard Manager" : { @@ -296,27 +278,15 @@ } } } + }, + "Configure how many recent clipboard items to remember (minimum 3)." : { + }, "Continue" : { - }, - "Controls" : { - }, "Copy something to get started" : { - }, - "CPU Usage" : { - - }, - "Current custom timer: %@" : { - - }, - "Current History" : { - - }, - "Current Items" : { - }, "Custom" : { @@ -340,27 +310,15 @@ }, "Custom notch size - %.0f" : { - }, - "Custom Timer" : { - - }, - "Custom Timer Duration" : { - }, "Custom vizualizers (Lottie)" : { - }, - "Custom: %@" : { - }, "Customize in Settings" : { }, "Default" : { - }, - "Default: dynamic.m4a" : { - }, "Description" : { @@ -370,12 +328,6 @@ }, "Disabled" : { - }, - "Disk Read" : { - - }, - "Disk Write" : { - }, "Download" : { @@ -421,9 +373,6 @@ }, "dynamic.island" : { - }, - "Each graph can be individually enabled or disabled. Network activity shows download/upload speeds, and disk I/O shows read/write speeds." : { - }, "Easily copy, store, and manage your most-used content. Upgrade now for advanced features like multi-item storage and quick access!" : { @@ -464,9 +413,6 @@ }, "Enable music live activity" : { - }, - "Enable or disable the timer functionality in the Dynamic Island. The live activity toggle controls whether timer progress is shown in the expanded view." : { - }, "Enable sneak peek" : { @@ -477,7 +423,13 @@ "Enable timer feature in Settings to use this tab" : { }, - "Enable timer live activity" : { + "Enable to access timer functionality in the Dynamic Island." : { + + }, + "Enable to monitor CPU, Memory, GPU, Network and Disk usage." : { + + }, + "Enable to track clipboard history and access recent items." : { }, "Enhance your experience with HUDs" : { @@ -539,9 +491,6 @@ }, "Got it!" : { - }, - "GPU Usage" : { - }, "Gradient" : { @@ -549,7 +498,7 @@ "Graph Visibility" : { }, - "Hide DynamicIsland Options" : { + "Hide BoringNotch Options" : { }, "Hide only when NowPlaying app is in fullscreen" : { @@ -561,7 +510,7 @@ "High" : { }, - "History Size" : { + "History size" : { }, "https://github.com/th-ch/youtube-music" : { @@ -592,9 +541,6 @@ }, "Installed extensions" : { - }, - "Last Updated" : { - }, "Later" : { @@ -604,9 +550,6 @@ }, "Live" : { - }, - "Live Performance Data" : { - }, "Lottie JSON URL" : { @@ -648,6 +591,9 @@ }, "Media change with horizontal gestures" : { + }, + "Media controls" : { + }, "Media inactivity timeout" : { "localizations" : { @@ -674,9 +620,6 @@ }, "Medium" : { - }, - "Memory Usage" : { - }, "Menubar icon" : { "extractionState" : "stale", @@ -697,21 +640,12 @@ }, "Minimum hover duration" : { - }, - "Minutes" : { - }, "Mirror" : { }, "Mirror shape" : { - }, - "Monitor clipboard changes and keep a history of recent copies. Use Cmd+Shift+V to quickly access clipboard history." : { - - }, - "Monitoring Status" : { - }, "More" : { @@ -727,12 +661,6 @@ }, "Need Restart" : { - }, - "Network Download" : { - - }, - "Network Upload" : { - }, "Never hide" : { @@ -884,9 +812,6 @@ }, "Remember last tab" : { - }, - "Reset to Default" : { - }, "Restart" : { @@ -910,9 +835,6 @@ } } } - }, - "Seconds" : { - }, "Select Calendars" : { @@ -925,9 +847,6 @@ }, "Selected animation" : { - }, - "Set a custom duration for the timer. This will be used when you press the 'Custom' button in the timer interface." : { - }, "Settings" : { "localizations" : { @@ -955,9 +874,6 @@ } } } - }, - "Show calendar" : { - }, "Show charging indicator" : { "extractionState" : "stale", @@ -997,6 +913,9 @@ }, "Show on all displays" : { + }, + "Show shuffle and repeat buttons" : { + }, "Slider color" : { @@ -1025,15 +944,6 @@ }, "Start" : { - }, - "Start Demo Timer:" : { - - }, - "Start Monitoring" : { - - }, - "Starts a 5-minute demo timer to test the timer live activity feature." : { - }, "Stats" : { @@ -1041,10 +951,10 @@ "Stats Disabled" : { }, - "Stop" : { + "Stats tab will appear in the notch when enabled." : { }, - "Stop Monitoring" : { + "Stop" : { }, "Stopped" : { @@ -1063,14 +973,11 @@ "System features" : { }, - "The clipboard icon appears in the header next to the settings gear when enabled. Use the global shortcut Cmd+Shift+V or click the icon to access history." : { + "System Monitoring" : { }, "TheDynamicIsland" : { - }, - "This will permanently delete all stored clipboard history. The clipboard button is located to the left of the settings gear in the header." : { - }, "Time to Full Charge: %lld min" : { @@ -1081,10 +988,7 @@ "Timer Disabled" : { }, - "Timer Feature" : { - - }, - "Timer Sound" : { + "Timer tab will appear in the notch when enabled." : { }, "Toggle Notch Open:" : { @@ -1107,9 +1011,6 @@ }, "unmuted" : { - }, - "Update" : { - }, "Upgrade to Pro" : { @@ -1145,12 +1046,6 @@ }, "What's New" : { - }, - "When enabled, the Stats tab will display real-time system performance graphs. This feature requires system permissions and may use additional battery." : { - - }, - "With %lld graphs enabled, the Dynamic Island will expand horizontally to accommodate all graphs in a single row." : { - }, "You can now enjoy the app. If you want to tweak things further, you can always visit the settings." : { diff --git a/DynamicIsland/MediaControllers/AppleMusicController.swift b/DynamicIsland/MediaControllers/AppleMusicController.swift index cfdf94d..5105d41 100644 --- a/DynamicIsland/MediaControllers/AppleMusicController.swift +++ b/DynamicIsland/MediaControllers/AppleMusicController.swift @@ -2,7 +2,7 @@ // AppleMusicController.swift // DynamicIsland // -// Created by Alexander Greco on 2025-03-29. +// Created by Alexander on 2025-03-29. // import Foundation @@ -15,110 +15,125 @@ class AppleMusicController: MediaControllerProtocol { bundleIdentifier: "com.apple.Music" ) - var playbackStatePublisher: Published.Publisher { $playbackState } + var playbackStatePublisher: AnyPublisher { + $playbackState.eraseToAnyPublisher() + } + + var isWorking: Bool { + return true // AppleMusic controller always works + } + + private var notificationTask: Task? // MARK: - Initialization init() { setupPlaybackStateChangeObserver() - DispatchQueue.main.async { [weak self] in - if self?.isActive() == true { - self?.updatePlaybackInfo() + Task { + if isActive() { + await updatePlaybackInfo() } } } private func setupPlaybackStateChangeObserver() { - DistributedNotificationCenter.default().addObserver( - self, - selector: #selector(updatePlaybackInfo), - name: NSNotification.Name("com.apple.Music.playerInfo"), - object: nil - ) + notificationTask = Task { @Sendable [weak self] in + let notifications = DistributedNotificationCenter.default().notifications( + named: NSNotification.Name("com.apple.Music.playerInfo") + ) + + for await _ in notifications { + await self?.updatePlaybackInfo() + } + } } deinit { - DistributedNotificationCenter.default().removeObserver( - self, - name: NSNotification.Name("com.apple.Music.playerInfo"), - object: nil - ) - + notificationTask?.cancel() } // MARK: - Protocol Implementation - func play() { - executeCommand("play") + func play() async { + await executeCommand("play") } - func pause() { - executeCommand("pause") + func pause() async { + await executeCommand("pause") } - func togglePlay() { - executeCommand("playpause") + func togglePlay() async { + await executeCommand("playpause") } - func nextTrack() { - executeCommand("next track") + func nextTrack() async { + await executeCommand("next track") } - func previousTrack() { - executeCommand("previous track") + func previousTrack() async { + await executeCommand("previous track") } - func seek(to time: Double) { - executeCommand("set player position to \(time)") - updatePlaybackInfo() + func seek(to time: Double) async { + await executeCommand("set player position to \(time)") + await updatePlaybackInfo() + } + + func toggleShuffle() async { + await executeCommand("set shuffle enabled to not shuffle enabled") + playbackState.isShuffled.toggle() + } + + func toggleRepeat() async { + await executeCommand(""" + if song repeat is off then + set song repeat to all + else if song repeat is all then + set song repeat to one + else + set song repeat to off + end if + """) + + if playbackState.repeatMode == .off { + playbackState.repeatMode = .all + } else if playbackState.repeatMode == .all { + playbackState.repeatMode = .one + } else { + playbackState.repeatMode = .off + } } func isActive() -> Bool { let runningApps = NSWorkspace.shared.runningApplications - return runningApps.contains { $0.bundleIdentifier == playbackState.bundleIdentifier } + return runningApps.contains { $0.bundleIdentifier == "com.apple.Music" } + } + + func updatePlaybackInfo() async { + guard let descriptor = try? await fetchPlaybackInfoAsync() else { return } + guard descriptor.numberOfItems >= 8 else { return } + var updatedState = self.playbackState + + updatedState.isPlaying = descriptor.atIndex(1)?.booleanValue ?? false + updatedState.title = descriptor.atIndex(2)?.stringValue ?? "Unknown" + updatedState.artist = descriptor.atIndex(3)?.stringValue ?? "Unknown" + updatedState.album = descriptor.atIndex(4)?.stringValue ?? "Unknown" + updatedState.currentTime = descriptor.atIndex(5)?.doubleValue ?? 0 + updatedState.duration = descriptor.atIndex(6)?.doubleValue ?? 0 + updatedState.isShuffled = descriptor.atIndex(7)?.booleanValue ?? false + let repeatModeValue = descriptor.atIndex(8)?.int32Value ?? 0 + updatedState.repeatMode = RepeatMode(rawValue: Int(repeatModeValue)) ?? .off + updatedState.artwork = descriptor.atIndex(9)?.data as Data? + + self.playbackState = updatedState } // MARK: - Private Methods - @objc func updatePlaybackInfo() { - guard let descriptor = fetchPlaybackInfo() else { return } - guard descriptor.numberOfItems >= 8 else { return } - - let isPlaying = descriptor.atIndex(1)?.booleanValue ?? false - let currentTrack = descriptor.atIndex(2)?.stringValue ?? "Unknown" - let currentTrackArtist = descriptor.atIndex(3)?.stringValue ?? "Unknown" - let currentTrackAlbum = descriptor.atIndex(4)?.stringValue ?? "Unknown" - let currentTime = descriptor.atIndex(5)?.doubleValue ?? 0 - let duration = descriptor.atIndex(6)?.doubleValue ?? 0 - let isShuffled = descriptor.atIndex(7)?.booleanValue ?? false - let isRepeating = descriptor.atIndex(8)?.booleanValue ?? false - let artworkData = descriptor.atIndex(9)?.data as Data? - - let updatedState = PlaybackState( - bundleIdentifier: "com.apple.Music", - isPlaying: isPlaying, - title: currentTrack, - artist: currentTrackArtist, - album: currentTrackAlbum, - currentTime: currentTime, - duration: duration, - playbackRate: 1, - isShuffled: isShuffled, - isRepeating: isRepeating, - lastUpdated: Date(), - artwork: (artworkData?.isEmpty ?? true) ? nil : artworkData - ) - - DispatchQueue.main.async { [weak self] in - self?.playbackState = updatedState - } - } - private func executeCommand(_ command: String) { + private func executeCommand(_ command: String) async { let script = "tell application \"Music\" to \(command)" - Task { - try? await AppleScriptHelper.executeVoid(script) - } + try? await AppleScriptHelper.executeVoid(script) } - private func fetchPlaybackInfo() -> NSAppleEventDescriptor? { + private func fetchPlaybackInfoAsync() async throws -> NSAppleEventDescriptor? { let script = """ tell application "Music" set isRunning to true @@ -129,26 +144,28 @@ class AppleMusicController: MediaControllerProtocol { set currentTrackAlbum to album of current track set trackPosition to player position set trackDuration to duration of current track - set shuffleState to false - set repeatState to false + set shuffleState to shuffle enabled + set repeatState to song repeat + if repeatState is off then + set repeatValue to 0 + else if repeatState is all then + set repeatValue to 1 + else if repeatState is one then + set repeatValue to 2 + end if + try set artData to data of artwork 1 of current track on error set artData to "" end try - return {playerState, currentTrackName, currentTrackArtist, currentTrackAlbum, trackPosition, trackDuration, shuffleState, repeatState, artData} + return {playerState, currentTrackName, currentTrackArtist, currentTrackAlbum, trackPosition, trackDuration, shuffleState, repeatValue, artData} on error - return {false, "Not Playing", "Unknown", "Unknown", 0, 0, false, false, ""} + return {false, "Not Playing", "Unknown", "Unknown", 0, 0, false, 0, ""} end try end tell """ - var descriptor: NSAppleEventDescriptor? = nil - let semaphore = DispatchSemaphore(value: 0) - Task { - descriptor = try? await AppleScriptHelper.execute(script) - semaphore.signal() - } - semaphore.wait() - return descriptor + + return try await AppleScriptHelper.execute(script) } } diff --git a/DynamicIsland/MediaControllers/MediaControllerProtocol.swift b/DynamicIsland/MediaControllers/MediaControllerProtocol.swift index 5d03c00..20f9a5e 100644 --- a/DynamicIsland/MediaControllers/MediaControllerProtocol.swift +++ b/DynamicIsland/MediaControllers/MediaControllerProtocol.swift @@ -6,16 +6,19 @@ // import Foundation -import AppKit +import Combine protocol MediaControllerProtocol: ObservableObject { - var playbackStatePublisher: Published.Publisher { get } - func play() - func pause() - func seek(to time: Double) - func nextTrack() - func previousTrack() - func togglePlay() + var playbackStatePublisher: AnyPublisher { get } + var isWorking: Bool { get } + func play() async + func pause() async + func seek(to time: Double) async + func nextTrack() async + func previousTrack() async + func togglePlay() async + func toggleShuffle() async + func toggleRepeat() async func isActive() -> Bool - func updatePlaybackInfo() + func updatePlaybackInfo() async } diff --git a/DynamicIsland/MediaControllers/NowPlayingController.swift b/DynamicIsland/MediaControllers/NowPlayingController.swift index a4c3b0a..bea429a 100644 --- a/DynamicIsland/MediaControllers/NowPlayingController.swift +++ b/DynamicIsland/MediaControllers/NowPlayingController.swift @@ -5,193 +5,222 @@ // Created by Alexander on 2025-03-29. // -import Combine import AppKit +import Combine import Foundation -class NowPlayingController: ObservableObject, MediaControllerProtocol { +final class NowPlayingController: ObservableObject, MediaControllerProtocol { + // Stub for now to conform with ControllerProtocol + func updatePlaybackInfo() {} + // MARK: - Properties - @Published var playbackState: PlaybackState = .init( + @Published private(set) var playbackState: PlaybackState = .init( bundleIdentifier: "com.apple.Music" ) + private var isResourcesAvailable = false + + var playbackStatePublisher: AnyPublisher { + $playbackState.eraseToAnyPublisher() + } - var playbackStatePublisher: Published.Publisher { $playbackState } - private var cancellables = Set() - private var lastMusicItem: (title: String, artist: String, album: String, duration: TimeInterval, artworkData: Data?)? - + var isWorking: Bool { + return isResourcesAvailable + } + private var lastMusicItem: + (title: String, artist: String, album: String, duration: TimeInterval, artworkData: Data?)? + // MARK: - Media Remote Functions private let mediaRemoteBundle: CFBundle - private let MRMediaRemoteGetNowPlayingInfo: @convention(c) (DispatchQueue, @escaping ([String: Any]) -> Void) -> Void - private let MRMediaRemoteRegisterForNowPlayingNotifications: @convention(c) (DispatchQueue) -> Void - private let MRMediaRemoteGetNowPlayingApplicationIsPlaying: @convention(c) (DispatchQueue, @escaping (Bool) -> Void) -> Void - private let MRMediaRemoteGetNowPlayingClient: @convention(c) (DispatchQueue, @escaping (AnyObject?) -> Void) -> Void - private let MRNowPlayingClientGetBundleIdentifier: @convention(c) (AnyObject?) -> String? - private let MRNowPlayingClientGetParentAppBundleIdentifier: @convention(c) (AnyObject?) -> String? - private let MRMediaRemoteSendCommandFunction:@convention(c) (Int, AnyObject?) -> Void + private let MRMediaRemoteSendCommandFunction: @convention(c) (Int, AnyObject?) -> Void private let MRMediaRemoteSetElapsedTimeFunction: @convention(c) (Double) -> Void + private let MRMediaRemoteSetShuffleModeFunction: @convention(c) (Int) -> Void + private let MRMediaRemoteSetRepeatModeFunction: @convention(c) (Int) -> Void + + private var process: Process? + private var pipe: Pipe? + private var buffer = "" // MARK: - Initialization init?() { - guard let bundle = CFBundleCreate(kCFAllocatorDefault, NSURL(fileURLWithPath: "/System/Library/PrivateFrameworks/MediaRemote.framework")), - let MRMediaRemoteGetNowPlayingInfoPointer = CFBundleGetFunctionPointerForName(bundle, "MRMediaRemoteGetNowPlayingInfo" as CFString), - let MRMediaRemoteRegisterForNowPlayingNotificationsPointer = CFBundleGetFunctionPointerForName(bundle, "MRMediaRemoteRegisterForNowPlayingNotifications" as CFString), - let MRMediaRemoteGetNowPlayingApplicationIsPlayingPointer = CFBundleGetFunctionPointerForName(bundle, "MRMediaRemoteGetNowPlayingApplicationIsPlaying" as CFString), - let MRMediaRemoteGetNowPlayingClientPointer = CFBundleGetFunctionPointerForName(bundle, "MRMediaRemoteGetNowPlayingClient" as CFString), - let MRNowPlayingClientGetBundleIdentifierPointer = CFBundleGetFunctionPointerForName(bundle, "MRNowPlayingClientGetBundleIdentifier" as CFString), - let MRNowPlayingClientGetParentAppBundleIdentifierPointer = CFBundleGetFunctionPointerForName(bundle, "MRNowPlayingClientGetParentAppBundleIdentifier" as CFString), - let MRMediaRemoteSendCommandPointer = CFBundleGetFunctionPointerForName(bundle, "MRMediaRemoteSendCommand" as CFString), - let MRMediaRemoteSetElapsedTimePointer = CFBundleGetFunctionPointerForName(bundle, "MRMediaRemoteSetElapsedTime" as CFString) + guard + let bundle = CFBundleCreate( + kCFAllocatorDefault, + NSURL(fileURLWithPath: "/System/Library/PrivateFrameworks/MediaRemote.framework")), + let MRMediaRemoteSendCommandPointer = CFBundleGetFunctionPointerForName( + bundle, "MRMediaRemoteSendCommand" as CFString), + let MRMediaRemoteSetElapsedTimePointer = CFBundleGetFunctionPointerForName( + bundle, "MRMediaRemoteSetElapsedTime" as CFString), + let MRMediaRemoteSetShuffleModePointer = CFBundleGetFunctionPointerForName( + bundle, "MRMediaRemoteSetShuffleMode" as CFString), + let MRMediaRemoteSetRepeatModePointer = CFBundleGetFunctionPointerForName( + bundle, "MRMediaRemoteSetRepeatMode" as CFString) + else { return nil } mediaRemoteBundle = bundle - MRMediaRemoteGetNowPlayingInfo = unsafeBitCast(MRMediaRemoteGetNowPlayingInfoPointer, to: (@convention(c) (DispatchQueue, @escaping ([String: Any]) -> Void) -> Void).self) - MRMediaRemoteRegisterForNowPlayingNotifications = unsafeBitCast(MRMediaRemoteRegisterForNowPlayingNotificationsPointer, to: (@convention(c) (DispatchQueue) -> Void).self) - MRMediaRemoteGetNowPlayingApplicationIsPlaying = unsafeBitCast(MRMediaRemoteGetNowPlayingApplicationIsPlayingPointer, to: (@convention(c) (DispatchQueue, @escaping (Bool) -> Void) -> Void).self) - MRMediaRemoteGetNowPlayingClient = unsafeBitCast(MRMediaRemoteGetNowPlayingClientPointer, to: (@convention(c) (DispatchQueue, @escaping (AnyObject?) -> Void) -> Void).self) - MRNowPlayingClientGetBundleIdentifier = unsafeBitCast(MRNowPlayingClientGetBundleIdentifierPointer, to: (@convention(c) (AnyObject?) -> String?).self) - MRNowPlayingClientGetParentAppBundleIdentifier = unsafeBitCast(MRNowPlayingClientGetParentAppBundleIdentifierPointer, to: (@convention(c) (AnyObject?) -> String?).self) - MRMediaRemoteSendCommandFunction = unsafeBitCast(MRMediaRemoteSendCommandPointer, to: (@convention(c) (Int, AnyObject?) -> Void).self) - MRMediaRemoteSetElapsedTimeFunction = unsafeBitCast(MRMediaRemoteSetElapsedTimePointer, to: (@convention(c) (Double) -> Void).self) - + MRMediaRemoteSendCommandFunction = unsafeBitCast( + MRMediaRemoteSendCommandPointer, to: (@convention(c) (Int, AnyObject?) -> Void).self) + MRMediaRemoteSetElapsedTimeFunction = unsafeBitCast( + MRMediaRemoteSetElapsedTimePointer, to: (@convention(c) (Double) -> Void).self) + MRMediaRemoteSetShuffleModeFunction = unsafeBitCast( + MRMediaRemoteSetShuffleModePointer, to: (@convention(c) (Int) -> Void).self) + MRMediaRemoteSetRepeatModeFunction = unsafeBitCast( + MRMediaRemoteSetRepeatModePointer, to: (@convention(c) (Int) -> Void).self) + setupNowPlayingObserver() updatePlaybackInfo() } - + deinit { - // Clean up all Combine subscribers - cancellables.removeAll() - - // Remove distributed notification observers - DistributedNotificationCenter.default().removeObserver( - self, - name: NSNotification.Name("com.spotify.client.PlaybackStateChanged"), - object: nil - ) - - DistributedNotificationCenter.default().removeObserver( - self, - name: NSNotification.Name("com.apple.Music.playerInfo"), - object: nil - ) + if let process = self.process { + if process.isRunning { + process.terminate() + process.waitUntilExit() + } + } + + if let pipe = self.pipe { + pipe.fileHandleForReading.closeFile() + pipe.fileHandleForWriting.closeFile() + } + + self.process = nil + self.pipe = nil } - + // MARK: - Protocol Implementation - func play() { + func play() async { MRMediaRemoteSendCommandFunction(0, nil) } - - func pause() { + + func pause() async { MRMediaRemoteSendCommandFunction(1, nil) } - - func togglePlay() { + + func togglePlay() async { MRMediaRemoteSendCommandFunction(2, nil) } - - func nextTrack() { + + func nextTrack() async { MRMediaRemoteSendCommandFunction(4, nil) } - - func previousTrack() { + + func previousTrack() async { MRMediaRemoteSendCommandFunction(5, nil) } - - func seek(to time: Double) { + + func seek(to time: Double) async { MRMediaRemoteSetElapsedTimeFunction(time) } - + func isActive() -> Bool { return true } - // MARK: - Setup Methods - private func setupNowPlayingObserver() { - MRMediaRemoteRegisterForNowPlayingNotifications(DispatchQueue.main) - - NotificationCenter.default.publisher(for: NSNotification.Name("kMRMediaRemoteNowPlayingInfoDidChangeNotification")) - .sink { [weak self] _ in self?.updatePlaybackInfo() } - .store(in: &cancellables) - - NotificationCenter.default.publisher(for: NSNotification.Name("kMRMediaRemoteNowPlayingApplicationDidChangeNotification")) - .sink { [weak self] _ in self?.updateApp() } - .store(in: &cancellables) - - DistributedNotificationCenter.default().addObserver( - self, - selector: #selector(updatePlaybackInfo), - name: NSNotification.Name("com.spotify.client.PlaybackStateChanged"), - object: nil - ) - - DistributedNotificationCenter.default().addObserver( - self, - selector: #selector(updatePlaybackInfo), - name: NSNotification.Name("com.apple.Music.playerInfo"), - object: nil - ) + func toggleShuffle() async { + // MRMediaRemoteSendCommandFunction(6, nil) + MRMediaRemoteSetShuffleModeFunction(playbackState.isShuffled ? 3 : 1) + playbackState.isShuffled.toggle() } - // MARK: - Update Methods - @objc func updateApp() { - // Get the now playing client - MRMediaRemoteGetNowPlayingClient(DispatchQueue.main) { [weak self] clientObj in - guard let clientObj = clientObj else { - DispatchQueue.main.async { - self?.playbackState.bundleIdentifier = "com.apple.Music" // Default fallback - } - return - } - - // Try to get parent bundle ID first, then fall back to direct bundle ID - var appBundleID = self?.MRNowPlayingClientGetParentAppBundleIdentifier(clientObj) - if appBundleID == nil { - appBundleID = self?.MRNowPlayingClientGetBundleIdentifier(clientObj) - } - - // Special case for WebKit.GPU which is often Safari - if appBundleID == "com.apple.WebKit.GPU" { - appBundleID = "com.apple.Safari" - } - - DispatchQueue.main.async { - self?.playbackState.bundleIdentifier = appBundleID ?? "com.apple.Music" - } - } + func toggleRepeat() async { + // MRMediaRemoteSendCommandFunction(7, nil) + let newRepeatMode = (playbackState.repeatMode == .off) ? 3 : (playbackState.repeatMode.rawValue - 1) + playbackState.repeatMode = RepeatMode(rawValue: newRepeatMode) ?? .off + MRMediaRemoteSetRepeatModeFunction(newRepeatMode) } + + // MARK: - Setup Methods + private func setupNowPlayingObserver() { + let process = Process() + guard + let scriptURL = Bundle.main.url(forResource: "mediaremote-adapter", withExtension: "pl"), + let frameworkPath = Bundle.main.privateFrameworksPath?.appending("/MediaRemoteAdapter.framework") + else { + print("⚠️ Could not find mediaremote-adapter.pl script or framework path - NowPlaying controller disabled") + isResourcesAvailable = false + return + } + + isResourcesAvailable = true + process.executableURL = URL(fileURLWithPath: "/usr/bin/perl") + process.arguments = [scriptURL.path, frameworkPath, "stream"] + let pipe = Pipe() + process.standardOutput = pipe + self.process = process + self.pipe = pipe - @objc func updatePlaybackInfo() { - updateApp() - - MRMediaRemoteGetNowPlayingInfo(DispatchQueue.main) { [weak self] information in - guard let self = self else { return } - - let title = information["kMRMediaRemoteNowPlayingInfoTitle"] as? String ?? "" - let artist = information["kMRMediaRemoteNowPlayingInfoArtist"] as? String ?? "" - let album = information["kMRMediaRemoteNowPlayingInfoAlbum"] as? String ?? "" - let duration = information["kMRMediaRemoteNowPlayingInfoDuration"] as? TimeInterval ?? 0 - let currentTime = information["kMRMediaRemoteNowPlayingInfoElapsedTime"] as? TimeInterval ?? 0 - let artworkData = information["kMRMediaRemoteNowPlayingInfoArtworkData"] as? Data - let timestamp = information["kMRMediaRemoteNowPlayingInfoTimestamp"] as? Date ?? Date() - let playbackRate = information["kMRMediaRemoteNowPlayingInfoPlaybackRate"] as? Double ?? 1 - - // Update playback state - DispatchQueue.main.async { - self.playbackState.title = title - self.playbackState.artist = artist - self.playbackState.album = album - self.playbackState.duration = duration - self.playbackState.currentTime = currentTime - self.playbackState.artwork = artworkData - self.playbackState.lastUpdated = timestamp - self.playbackState.playbackRate = playbackRate - - // Check playback state - self.MRMediaRemoteGetNowPlayingApplicationIsPlaying(DispatchQueue.main) { [weak self] isPlaying in - DispatchQueue.main.async { - self?.playbackState.isPlaying = isPlaying + pipe.fileHandleForReading.readabilityHandler = { [weak self] handle in + let data = handle.availableData + guard !data.isEmpty else { return } + if let chunk = String(data: data, encoding: .utf8) { + self?.buffer.append(chunk) + while let range = self?.buffer.range(of: "\n") { + let line = String(self?.buffer[...Publisher { $playbackState } + var playbackStatePublisher: AnyPublisher { + $playbackState.eraseToAnyPublisher() + } + + var isWorking: Bool { + return true // Spotify controller always works + } + + private var notificationTask: Task? + + //Constant for time between command and update + let commandUpdateDelay: Duration = .milliseconds(25) init() { setupPlaybackStateChangeObserver() - DispatchQueue.main.async { [weak self] in - if self?.isActive() == true { - self?.updatePlaybackInfo() + Task { + if isActive() { + await updatePlaybackInfo() } } } private func setupPlaybackStateChangeObserver() { - DistributedNotificationCenter.default().addObserver( - self, - selector: #selector(updatePlaybackInfo), - name: NSNotification.Name("com.spotify.client.PlaybackStateChanged"), - object: nil - ) + notificationTask = Task { @Sendable [weak self] in + let notifications = DistributedNotificationCenter.default().notifications( + named: NSNotification.Name("com.spotify.client.PlaybackStateChanged") + ) + + for await _ in notifications { + await self?.updatePlaybackInfo() + } + } } deinit { - // Remove notification observer when controller is deallocated - DistributedNotificationCenter.default().removeObserver( - self, - name: NSNotification.Name("com.spotify.client.PlaybackStateChanged"), - object: nil - ) + notificationTask?.cancel() } // MARK: - Protocol Implementation - func play() { - executeCommand("play") + func play() async { + await executeCommand("play") } - func pause() { - executeCommand("pause") + func pause() async { + await executeCommand("pause") } - func togglePlay() { - executeCommand("playpause") + func togglePlay() async { + await executeCommand("playpause") } - func nextTrack() { - executeCommand("next track") + func nextTrack() async { + await executeCommand("next track") } - func previousTrack() { - executeCommand("previous track") + func previousTrack() async { + await executeCommand("previous track") } - func seek(to time: Double) { - executeCommand("set player position to \(time)") - updatePlaybackInfo() + func seek(to time: Double) async { + await executeCommand("set player position to \(time)") + try? await Task.sleep(for: commandUpdateDelay) + await updatePlaybackInfo() + } + + func toggleShuffle() async { + await executeCommand("set shuffling to not shuffling") + try? await Task.sleep(for: commandUpdateDelay) + await updatePlaybackInfo() + } + + func toggleRepeat() async { + await executeCommand("set repeating to not repeating") + try? await Task.sleep(for: commandUpdateDelay) + await updatePlaybackInfo() } func isActive() -> Bool { @@ -75,9 +97,8 @@ class SpotifyController: MediaControllerProtocol { return runningApps.contains { $0.bundleIdentifier == playbackState.bundleIdentifier } } - // MARK: - Private Methods - @objc func updatePlaybackInfo() { - guard let descriptor = fetchPlaybackInfo() else { return } + func updatePlaybackInfo() async { + guard let descriptor = try? await fetchPlaybackInfoAsync() else { return } guard descriptor.numberOfItems >= 9 else { return } let isPlaying = descriptor.atIndex(1)?.booleanValue ?? false @@ -100,42 +121,35 @@ class SpotifyController: MediaControllerProtocol { duration: duration, playbackRate: 1, isShuffled: isShuffled, - isRepeating: isRepeating, + repeatMode: isRepeating ? .all : .off, lastUpdated: Date() ) - DispatchQueue.main.async { [weak self] in - self?.playbackState = state - } + self.playbackState = state // Load artwork asynchronously and update the state when complete if !artworkURL.isEmpty, let url = URL(string: artworkURL) { - DispatchQueue.global(qos: .background).async { [weak self] in - do { - let artworkData = try Data(contentsOf: url) - DispatchQueue.main.async { - // Create a new state with the artwork data and update - var updatedState = state - updatedState.artwork = artworkData - self?.playbackState = updatedState - } - } catch { - print("Failed to load artwork: \(error)") - } + let currentState = state + do { + let (data, _) = try await URLSession.shared.data(from: url) + // Create a new state with the artwork data and update + var updatedState = currentState + updatedState.artwork = data + playbackState = updatedState + } catch { + print("Failed to load artwork: \(error)") } - } else { - self.playbackState = state } } - private func executeCommand(_ command: String) { +// MARK: - Private Methods + + private func executeCommand(_ command: String) async { let script = "tell application \"Spotify\" to \(command)" - Task { - try? await AppleScriptHelper.executeVoid(script) - } + try? await AppleScriptHelper.executeVoid(script) } - private func fetchPlaybackInfo() -> NSAppleEventDescriptor? { + private func fetchPlaybackInfoAsync() async throws -> NSAppleEventDescriptor? { let script = """ tell application "Spotify" set isRunning to true @@ -156,13 +170,6 @@ class SpotifyController: MediaControllerProtocol { end tell """ - var descriptor: NSAppleEventDescriptor? = nil - let semaphore = DispatchSemaphore(value: 0) - Task { - descriptor = try? await AppleScriptHelper.execute(script) - semaphore.signal() - } - semaphore.wait() - return descriptor + return try await AppleScriptHelper.execute(script) } } diff --git a/DynamicIsland/MediaControllers/YouTubeMusicController.swift b/DynamicIsland/MediaControllers/YouTubeMusicController.swift index d5cb82d..5a2d3cb 100644 --- a/DynamicIsland/MediaControllers/YouTubeMusicController.swift +++ b/DynamicIsland/MediaControllers/YouTubeMusicController.swift @@ -3,6 +3,7 @@ // DynamicIsland // // Created By Alexander on 2025-03-30. +// Modified by Pranav on 2025-06-16. // import Foundation @@ -12,143 +13,129 @@ import SwiftUI class YouTubeMusicController: MediaControllerProtocol { // MARK: - Properties @Published var playbackState: PlaybackState = .init( - bundleIdentifier: "com.github.th-ch.youtube-music", - isPlaying: true + bundleIdentifier: "com.github.th-ch.youtube-music" ) - var playbackStatePublisher: Published.Publisher { $playbackState } + var playbackStatePublisher: AnyPublisher { + $playbackState.eraseToAnyPublisher() + } + + var isWorking: Bool { + return true // YouTube Music controller always works + } private let baseURL = "http://localhost:26538" private var accessToken: String? - private var refreshTimer: Timer? + private var periodicUpdateTask: Task? + private var notificationTask: Task? private var cancellables = Set() - private var playbackInfoCancellable: AnyCancellable? private var isAuthenticating = false - private let authQueue = DispatchQueue(label: "com.dynamicisland.youtubemusicauth", qos: .background) init() { setupAppStateObservers() - authQueue.async { [weak self] in - self?.authenticateAndSetup() - } + Task {await authenticateAndSetup()} } deinit { stopPeriodicUpdates() + notificationTask?.cancel() cancellables.forEach { $0.cancel() } - NSWorkspace.shared.notificationCenter.removeObserver(self) } // MARK: - Authentication - private func authenticateAndSetup() { + private func authenticateAndSetup() async { // Prevent multiple concurrent authentication attempts guard !isAuthenticating else { return } isAuthenticating = true - getAccessToken { [weak self] success in - guard let self = self else { return } - - DispatchQueue.main.async { - self.isAuthenticating = false - if success { - self.startPeriodicUpdates() - self.updatePlaybackInfo() - } else { - // Retry authentication after a delay if it fails - DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 5) { [weak self] in - self?.authenticateAndSetup() - } - } - } + let success = await getAccessToken() + + self.isAuthenticating = false + if success { + await self.startPeriodicUpdates() + await updatePlaybackInfo() + } else { + try? await Task.sleep(for: .seconds(5)) + await authenticateAndSetup() } } - private func getAccessToken(completion: @escaping (Bool) -> Void) { - guard let url = URL(string: "\(baseURL)/auth/DynamicIsland") else { - completion(false) - return + private func getAccessToken() async -> Bool { + guard let url = URL(string: "\(baseURL)/auth/boringNotch") else { + return false } var request = URLRequest(url: url) request.httpMethod = "POST" - URLSession.shared.dataTaskPublisher(for: request) - .map { data, response -> Data in - return data - } - .decode(type: AuthResponse.self, decoder: JSONDecoder()) - .receive(on: DispatchQueue.main) - .sink(receiveCompletion: { completionStatus in - switch completionStatus { - case .failure(_): - completion(false) - case .finished: - break - } - }, receiveValue: { [weak self] response in - self?.accessToken = response.accessToken - completion(true) - }) - .store(in: &cancellables) + do { + let (data, _) = try await URLSession.shared.data(for: request) + let response = try JSONDecoder().decode(AuthResponse.self, from: data) + accessToken = response.accessToken + return true + } catch { + return false + } } // MARK: - Protocol Implementation - func play() { - sendCommand(endpoint: "/play", method: "POST") - updatePlaybackInfo() + func play() async { + await sendCommand(endpoint: "/play", method: "POST") + await updatePlaybackInfo() } - func pause() { - sendCommand(endpoint: "/pause", method: "POST") - updatePlaybackInfo() + func pause() async { + await sendCommand(endpoint: "/pause", method: "POST") + await updatePlaybackInfo() } - func togglePlay() { + func togglePlay() async { if !isActive() { if let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: playbackState.bundleIdentifier) { NSWorkspace.shared.open(url) } } - sendCommand(endpoint: "/toggle-play", method: "POST") - updatePlaybackInfo() + await sendCommand(endpoint: "/toggle-play", method: "POST") + await updatePlaybackInfo() } - func nextTrack() { - sendCommand(endpoint: "/next", method: "POST") - updatePlaybackInfo() + func nextTrack() async { + await sendCommand(endpoint: "/next", method: "POST") + await updatePlaybackInfo() } - func previousTrack() { - sendCommand(endpoint: "/previous", method: "POST") - updatePlaybackInfo() + func previousTrack() async { + await sendCommand(endpoint: "/previous", method: "POST") + await updatePlaybackInfo() } - func seek(to time: Double) { - guard let url = URL(string: "\(baseURL)/seek-to") else { return } - - var request = URLRequest(url: url) - request.httpMethod = "POST" - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - - if let token = accessToken { - request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") - } else { - authenticateAndSetup() - return - } - - // Create the request body with the seconds value + func seek(to time: Double) async { + // Format the seek data payload according to the API schema let seekData = ["seconds": time] do { - request.httpBody = try JSONEncoder().encode(seekData) - } catch { - return - } - - URLSession.shared.dataTaskPublisher(for: request) - .tryMap { data, response -> Data in + // Convert seek data to JSON + let jsonData = try JSONEncoder().encode(seekData) + + guard let url = URL(string: "\(baseURL)/api/v1/seek-to") else { return } + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + + if let token = accessToken { + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + } else { + await authenticateAndSetup() + return + } + + request.httpBody = jsonData + + do { + let (_, response) = try await URLSession.shared.data(for: request) + guard let httpResponse = response as? HTTPURLResponse else { throw URLError(.badServerResponse) } @@ -159,22 +146,44 @@ class YouTubeMusicController: MediaControllerProtocol { throw URLError(.badServerResponse) } - return data - } - .receive(on: DispatchQueue.main) - .sink(receiveCompletion: { [weak self] completion in - if case .failure(let error) = completion { - if let urlError = error as? URLError, urlError.code == .userAuthenticationRequired { - self?.accessToken = nil - self?.authenticateAndSetup() - } + // Update playback info after seeking + try? await Task.sleep(for: .milliseconds(300)) + await updatePlaybackInfo() + + } catch { + print("Seek error: \(error)") + if let urlError = error as? URLError, urlError.code == .userAuthenticationRequired { + self.accessToken = nil + await self.authenticateAndSetup() } - }, receiveValue: { [weak self] _ in - self?.updatePlaybackInfo() - }) - .store(in: &cancellables) + } + + } catch { + print("Error encoding seek data: \(error)") + return + } } + func fetchShuffleState() async { + await sendCommand(endpoint: "/shuffle", method: "GET") + } + + func toggleShuffle() async { + await sendCommand(endpoint: "/shuffle", method: "POST") + try? await Task.sleep(for: .milliseconds(200)) + await fetchShuffleState() + } + + func toggleRepeat() async { + await sendCommand(endpoint: "/switch-repeat", method: "POST") + try? await Task.sleep(for: .milliseconds(200)) + await fetchRepeatMode() + } + + func fetchRepeatMode() async { + await sendCommand(endpoint: "/repeat-mode", method: "GET") + } + func isActive() -> Bool { let runningApps = NSWorkspace.shared.runningApplications return runningApps.contains { $0.bundleIdentifier == playbackState.bundleIdentifier } @@ -183,131 +192,170 @@ class YouTubeMusicController: MediaControllerProtocol { // MARK: - Private Methods private func setupAppStateObservers() { - // Register for app launch notifications - NSWorkspace.shared.notificationCenter.addObserver( - self, - selector: #selector(handleAppStateChange), - name: NSWorkspace.didLaunchApplicationNotification, - object: nil - ) - - // Register for app termination notifications - NSWorkspace.shared.notificationCenter.addObserver( - self, - selector: #selector(handleAppStateChange), - name: NSWorkspace.didTerminateApplicationNotification, - object: nil - ) + notificationTask = Task { @Sendable [weak self] in + await withTaskGroup(of: Void.self) { group in + // Handle app launch notifications + group.addTask { + let launchNotifications = NSWorkspace.shared.notificationCenter.notifications( + named: NSWorkspace.didLaunchApplicationNotification + ) + + for await notification in launchNotifications { + await self?.handleAppStateChange(notification: notification) + } + } + + // Handle app termination notifications + group.addTask { + let terminateNotifications = NSWorkspace.shared.notificationCenter.notifications( + named: NSWorkspace.didTerminateApplicationNotification + ) + + for await notification in terminateNotifications { + await self?.handleAppStateChange(notification: notification) + } + } + } + } } - @objc private func handleAppStateChange(notification: Notification) { + private func handleAppStateChange(notification: Notification) async { guard let app = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication, app.bundleIdentifier == playbackState.bundleIdentifier else { return } if notification.name == NSWorkspace.didLaunchApplicationNotification { - startPeriodicUpdates() + await startPeriodicUpdates() } else if notification.name == NSWorkspace.didTerminateApplicationNotification { // App has terminated - update state and stop timer stopPeriodicUpdates() - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - self.playbackState = PlaybackState( - bundleIdentifier: self.playbackState.bundleIdentifier, - isPlaying: false - ) - } + self.playbackState = PlaybackState( + bundleIdentifier: self.playbackState.bundleIdentifier, + isPlaying: false + ) } } - private func startPeriodicUpdates() { + private func startPeriodicUpdates() async { stopPeriodicUpdates() // Ensure we don't create multiple timers // Only start the timer if the app is actually running guard isActive() else { return } - refreshTimer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true) { [weak self] _ in - self?.updatePlaybackInfo() + periodicUpdateTask = Task { [weak self] in + while !Task.isCancelled { + await self?.updatePlaybackInfo() + try? await Task.sleep(for: .seconds(2)) + } } - refreshTimer?.tolerance = 0.5 // Initial update when timer starts - updatePlaybackInfo() + await updatePlaybackInfo() + await fetchRepeatMode() + await fetchShuffleState() } private func stopPeriodicUpdates() { - refreshTimer?.invalidate() - refreshTimer = nil + periodicUpdateTask?.cancel() + periodicUpdateTask = nil + } + + private func updateRepeatMode(_ modeString: String?) { + guard let modeString = modeString else { return } + + let repeatMode: RepeatMode + switch modeString { + case "NONE": + repeatMode = .off + case "ALL": + repeatMode = .all + case "ONE": + repeatMode = .one + default: + repeatMode = .off + } + + if repeatMode != playbackState.repeatMode { + playbackState.repeatMode = repeatMode + } } - func updatePlaybackInfo() { + func updatePlaybackInfo() async { if !isActive() { playbackState = PlaybackState(bundleIdentifier: playbackState.bundleIdentifier, isPlaying: false) stopPeriodicUpdates() return } - guard let request = createAuthenticatedRequest(for: "/api/v1/song") else { return } + + guard let request = await createAuthenticatedRequest(for: "/api/v1/song") else { return } - let backgroundQueue = DispatchQueue.global(qos: .utility) - - backgroundQueue.async { [weak self] in - guard let self = self else { return } + do { + let (data, response) = try await URLSession.shared.data(for: request) - playbackInfoCancellable?.cancel() + guard let httpResponse = response as? HTTPURLResponse else { + throw URLError(.badServerResponse) + } - playbackInfoCancellable = URLSession.shared.dataTaskPublisher(for: request) - .tryMap { data, response -> Data in - guard let httpResponse = response as? HTTPURLResponse else { - throw URLError(.badServerResponse) - } - - // Check for authentication errors - if httpResponse.statusCode == 401 || httpResponse.statusCode == 403 { - // Re-authenticate and retry later - DispatchQueue.main.async { [weak self] in - self?.accessToken = nil - self?.authenticateAndSetup() - } - throw URLError(.userAuthenticationRequired) - } else if httpResponse.statusCode < 200 || httpResponse.statusCode >= 300 { - throw URLError(.badServerResponse) - } - - return data - } - .decode(type: PlaybackResponse.self, decoder: JSONDecoder()) - .subscribe(on: backgroundQueue) - .receive(on: DispatchQueue.main) - .sink(receiveCompletion: { _ in }, receiveValue: { [weak self] response in - self?.updatePlaybackState(with: response) - }) + // Check for authentication errors + if httpResponse.statusCode == 401 || httpResponse.statusCode == 403 { + // Re-authenticate and retry later + self.accessToken = nil + await authenticateAndSetup() + throw URLError(.userAuthenticationRequired) + } else if httpResponse.statusCode < 200 || httpResponse.statusCode >= 300 { + throw URLError(.badServerResponse) + } + + let playbackResponse = try JSONDecoder().decode(PlaybackResponse.self, from: data) + await updatePlaybackState(with: playbackResponse) + + } catch { + // Handle error silently for now, as this is called periodically } } - private func updatePlaybackState(with response: PlaybackResponse) { - playbackState.isPlaying = !response.isPaused - playbackState.title = response.title - playbackState.artist = response.artist - playbackState.album = response.album ?? "" - playbackState.currentTime = response.elapsedSeconds - playbackState.duration = response.songDuration - playbackState.lastUpdated = Date() + private func updatePlaybackState(with response: PlaybackResponse) async { + var updatedState = self.playbackState + updatedState.isPlaying = !response.isPaused + updatedState.title = response.title + updatedState.artist = response.artist + updatedState.album = response.album ?? "" + updatedState.currentTime = response.elapsedSeconds + updatedState.duration = response.songDuration + updatedState.lastUpdated = Date() + + if let isShuffled = response.isShuffled { + updatedState.isShuffled = isShuffled + } // Load artwork if available if let artworkURL = response.imageSrc, let url = URL(string: artworkURL) { - DispatchQueue.global(qos: .background).async { [weak self] in - do { - let artworkData = try Data(contentsOf: url) - DispatchQueue.main.async { - self?.playbackState.artwork = artworkData - } - } catch { return } + do { + let artworkData = try Data(contentsOf: url) + playbackState.artwork = artworkData + + } catch { + NSLog("Failed to load artwork: \(error)") + updatedState.artwork = nil + self.playbackState = updatedState + return } } + self.playbackState = updatedState } - private func sendCommand(endpoint: String, method: String = "GET") { + func pollPlaybackState() async { + if !isActive() { + return + } + + await fetchRepeatMode() + await fetchShuffleState() + await updatePlaybackInfo() + } + + private func sendCommand(endpoint: String, method: String = "GET") async { guard let url = URL(string: "\(baseURL)/api/v1\(endpoint)") else { return } var request = URLRequest(url: url) @@ -316,49 +364,65 @@ class YouTubeMusicController: MediaControllerProtocol { if let token = accessToken { request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") } else { - authenticateAndSetup() + await authenticateAndSetup() return } - URLSession.shared.dataTaskPublisher(for: request) - .tryMap { data, response -> Data in - guard let httpResponse = response as? HTTPURLResponse else { - throw URLError(.badServerResponse) - } - - if httpResponse.statusCode == 401 || httpResponse.statusCode == 403 { - throw URLError(.userAuthenticationRequired) - } else if httpResponse.statusCode < 200 || httpResponse.statusCode >= 300 { - throw URLError(.badServerResponse) - } - - return data + do { + let (data, response) = try await URLSession.shared.data(for: request) + + guard let httpResponse = response as? HTTPURLResponse else { + throw URLError(.badServerResponse) } - .receive(on: DispatchQueue.main) - .sink(receiveCompletion: { [weak self] completion in - if case .failure(let error) = completion { - if let urlError = error as? URLError, urlError.code == .userAuthenticationRequired { - // Authentication error - token might be expired - self?.accessToken = nil - self?.authenticateAndSetup() - - // Try the command again after authentication - DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 2) { [weak self] in - self?.sendCommand(endpoint: endpoint, method: method) - } + + if httpResponse.statusCode == 401 || httpResponse.statusCode == 403 { + throw URLError(.userAuthenticationRequired) + } else if httpResponse.statusCode < 200 || httpResponse.statusCode >= 300 { + throw URLError(.badServerResponse) + } + + // Handle data based on endpoint + if endpoint == "/shuffle" || endpoint == "/shuffle-status" { + do { + if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any], + let shuffleState = json["state"] as? Bool { + playbackState.isShuffled = shuffleState } + } catch { + print("Error parsing shuffle state: \(error)") } - }, receiveValue: { [weak self] _ in - self?.updatePlaybackInfo() - }) - .store(in: &cancellables) + } else if endpoint == "/repeat-mode" { + do { + if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any], + let mode = json["mode"] as? String { + updateRepeatMode(mode) + } + } catch { + print("Error parsing repeat mode: \(error)") + } + } + + // Update playback info for relevant commands + await updatePlaybackInfo() + + } catch { + if let urlError = error as? URLError, urlError.code == .userAuthenticationRequired { + // Authentication error - token might be expired + accessToken = nil + await authenticateAndSetup() + + // Try the command again after authentication + try? await Task.sleep(for: .seconds(2)) + await sendCommand(endpoint: endpoint, method: method) + } + } } - private func createAuthenticatedRequest(for endpoint: String) -> URLRequest? { + private func createAuthenticatedRequest(for endpoint: String) async -> URLRequest? { guard let token = accessToken, let url = URL(string: "\(baseURL)\(endpoint)") else { if accessToken == nil { // Token is missing, try to authenticate again - authenticateAndSetup() + await authenticateAndSetup() } return nil } @@ -383,4 +447,6 @@ struct PlaybackResponse: Codable { let elapsedSeconds: Double let songDuration: Double let imageSrc: String? + let repeatMode: Int? + let isShuffled: Bool? } diff --git a/DynamicIsland/components/Calendar/DynamicIslandCalendar.swift b/DynamicIsland/components/Calendar/DynamicIslandCalendar.swift index 4dfd91f..9f3f750 100644 --- a/DynamicIsland/components/Calendar/DynamicIslandCalendar.swift +++ b/DynamicIsland/components/Calendar/DynamicIslandCalendar.swift @@ -150,7 +150,7 @@ struct WheelPicker: View { struct CalendarView: View { @EnvironmentObject var vm: DynamicIslandViewModel - @StateObject private var calendarManager = CalendarManager() + @ObservedObject private var calendarManager = CalendarManager.shared @State private var selectedDate = Date() var body: some View { diff --git a/DynamicIsland/components/Music/MusicVisualizer.swift b/DynamicIsland/components/Music/MusicVisualizer.swift index ffb769f..971fb1c 100644 --- a/DynamicIsland/components/Music/MusicVisualizer.swift +++ b/DynamicIsland/components/Music/MusicVisualizer.swift @@ -72,7 +72,9 @@ class AudioSpectrum: NSView { animation.autoreverses = true animation.fillMode = .forwards animation.isRemovedOnCompletion = false - + if #available(macOS 13.0, *) { + animation.preferredFrameRateRange = CAFrameRateRange(minimum: 24, maximum: 24, preferred: 24) + } barLayer.add(animation, forKey: "scaleY") } } diff --git a/DynamicIsland/components/Notch/DynamicIslandHeader.swift b/DynamicIsland/components/Notch/DynamicIslandHeader.swift index 05ffffc..cbdd9cb 100644 --- a/DynamicIsland/components/Notch/DynamicIslandHeader.swift +++ b/DynamicIsland/components/Notch/DynamicIslandHeader.swift @@ -46,11 +46,7 @@ struct DynamicIslandHeader: View { if vm.notchState == .open { if Defaults[.showMirror] { Button(action: { - if webcamManager.isSessionRunning { - webcamManager.stopSession() - } else { - webcamManager.startSession() - } + vm.toggleCameraPreview() }) { Capsule() .fill(.black) diff --git a/DynamicIsland/components/Notch/NotchHomeView.swift b/DynamicIsland/components/Notch/NotchHomeView.swift index aeeaf64..31a88e4 100644 --- a/DynamicIsland/components/Notch/NotchHomeView.swift +++ b/DynamicIsland/components/Notch/NotchHomeView.swift @@ -161,18 +161,58 @@ struct MusicControlsView: View { private var playbackControls: some View { HStack(spacing: 8) { + if Defaults[.showShuffleAndRepeat] { + HoverButton(icon: "shuffle", iconColor: musicManager.isShuffled ? .red : .white, scale: .medium) { + Task { + await MusicManager.shared.toggleShuffle() + } + } + } HoverButton(icon: "backward.fill", scale: .medium) { - MusicManager.shared.previousTrack() + Task { + await MusicManager.shared.previousTrack() + } } HoverButton(icon: musicManager.isPlaying ? "pause.fill" : "play.fill", scale: .large) { - MusicManager.shared.togglePlay() + Task { + await MusicManager.shared.togglePlay() + } } HoverButton(icon: "forward.fill", scale: .medium) { - MusicManager.shared.nextTrack() + Task { + await MusicManager.shared.nextTrack() + } + } + if Defaults[.showShuffleAndRepeat] { + HoverButton(icon: repeatIcon, iconColor: repeatIconColor, scale: .medium) { + Task { + await MusicManager.shared.toggleRepeat() + } + } } } .frame(maxWidth: .infinity, alignment: .center) } + + private var repeatIcon: String { + switch musicManager.repeatMode { + case .off: + return "repeat" + case .all: + return "repeat" + case .one: + return "repeat.1" + } + } + + private var repeatIconColor: Color { + switch musicManager.repeatMode { + case .off: + return .white + case .all, .one: + return .red + } + } } // MARK: - Main View diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index f2e2aed..a3a60a9 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -3,7 +3,7 @@ // DynamicIsland // // Created by Richard Kunkli on 07/08/2024. -// Modified by Hariharan Mudaliar +// import AVFoundation import Defaults @@ -14,20 +14,17 @@ import LottieUI import Sparkle import SwiftUI import SwiftUIIntrospect -import UniformTypeIdentifiers struct SettingsView: View { @StateObject var extensionManager = DynamicIslandExtensionManager() - @StateObject private var calendarManager = CalendarManager() - @State private var selectedTab = "General" - + let updaterController: SPUStandardUpdaterController? - + init(updaterController: SPUStandardUpdaterController? = nil) { self.updaterController = updaterController } - + var body: some View { NavigationSplitView { List(selection: $selectedTab) { @@ -40,9 +37,6 @@ struct SettingsView: View { NavigationLink(value: "Media") { Label("Media", systemImage: "play.laptopcomputer") } - NavigationLink(value: "Timer") { - Label("Timer", systemImage: "timer") - } NavigationLink(value: "Calendar") { Label("Calendar", systemImage: "calendar") } @@ -55,6 +49,15 @@ struct SettingsView: View { NavigationLink(value: "Battery") { Label("Battery", systemImage: "battery.100.bolt") } + NavigationLink(value: "Timer") { + Label("Timer", systemImage: "timer") + } + NavigationLink(value: "Stats") { + Label("Stats", systemImage: "chart.xyaxis.line") + } + NavigationLink(value: "Clipboard") { + Label("Clipboard", systemImage: "clipboard") + } if extensionManager.installedExtensions .contains(where: { $0.bundleIdentifier == downloadManagerExtension }) { NavigationLink(value: "Downloads") { @@ -64,12 +67,6 @@ struct SettingsView: View { NavigationLink(value: "Shelf") { Label("Shelf", systemImage: "books.vertical") } - NavigationLink(value: "Stats") { - Label("Stats", systemImage: "chart.line.uptrend.xyaxis") - } - NavigationLink(value: "Clipboard") { - Label("Clipboard", systemImage: "doc.on.clipboard") - } NavigationLink(value: "Shortcuts") { Label("Shortcuts", systemImage: "keyboard") } @@ -92,22 +89,22 @@ struct SettingsView: View { Appearance() case "Media": Media() - case "Timer": - TimerSettings() case "Calendar": CalendarSettings() case "HUD": HUD() case "Battery": Charge() + case "Timer": + TimerSettings() + case "Stats": + StatsSettings() + case "Clipboard": + ClipboardSettings() case "Downloads": Downloads() case "Shelf": Shelf() - case "Stats": - Stats() - case "Clipboard": - ClipboardSettings() case "Shortcuts": Shortcuts() case "Extensions": @@ -134,7 +131,6 @@ struct SettingsView: View { .disabled(true) } .environmentObject(extensionManager) - .environmentObject(calendarManager) .formStyle(.grouped) .frame(width: 700) .background(Color(NSColor.windowBackgroundColor)) @@ -158,8 +154,7 @@ struct GeneralSettings: View { @Default(.automaticallySwitchDisplay) var automaticallySwitchDisplay @Default(.enableGestures) var enableGestures @Default(.openNotchOnHover) var openNotchOnHover - @Default(.alwaysHideInFullscreen) var alwaysHideInFullscreen - + var body: some View { Form { Section { @@ -188,7 +183,7 @@ struct GeneralSettings: View { } header: { Text("System features") } - + Section { Picker(selection: $notchHeightMode, label: Text("Notch display height")) { @@ -248,9 +243,9 @@ struct GeneralSettings: View { } header: { Text("Notch Height") } - + NotchBehaviour() - + gestureControls() } .toolbar { @@ -266,7 +261,7 @@ struct GeneralSettings: View { } } } - + @ViewBuilder func gestureControls() -> some View { Section { @@ -297,7 +292,7 @@ struct GeneralSettings: View { .font(.caption) } } - + @ViewBuilder func NotchBehaviour() -> some View { Section { @@ -369,7 +364,7 @@ struct Downloads: View { Text("Both") .tag(DownloadIconStyle.iconAndAppIcon) } - + } header: { HStack { Text("Download indicators") @@ -397,7 +392,7 @@ struct Downloads: View { .contentShape(Rectangle()) .foregroundStyle(.secondary) } - + Divider() Button {} label: { Image(systemName: "minus") @@ -470,7 +465,7 @@ struct Media: View { @Default(.hideNotchOption) var hideNotchOption @Default(.enableSneakPeek) private var enableSneakPeek @Default(.sneakPeekStyles) var sneakPeekStyles - + var body: some View { Form { Section { @@ -503,13 +498,21 @@ struct Media: View { .font(.caption) } } + Section { + Defaults.Toggle(key: .showShuffleAndRepeat) { + HStack { + Text("Show shuffle and repeat buttons") + customBadge(text: "Beta") + } + } + } header: { + Text("Media controls") + } Section { Toggle( "Enable music live activity", - isOn: $coordinator.musicLiveActivityEnabled + isOn: $coordinator.musicLiveActivityEnabled.animation() ) - .animation(.easeInOut, value: coordinator.musicLiveActivityEnabled) - Toggle("Enable sneak peek", isOn: $enableSneakPeek) Picker("Sneak Peek Style", selection: $sneakPeekStyles){ ForEach(SneakPeekStyle.allCases) { style in @@ -532,21 +535,20 @@ struct Media: View { Picker(selection: $hideNotchOption, label: HStack { - Text("Hide DynamicIsland Options") + Text("Hide BoringNotch Options") customBadge(text: "Beta") }) { Text("Always hide in fullscreen").tag(HideNotchOption.always) Text("Hide only when NowPlaying app is in fullscreen").tag(HideNotchOption.nowPlayingOnly) Text("Never hide").tag(HideNotchOption.never) } - .onChange(of: hideNotchOption) { _, newValue in - Defaults[.alwaysHideInFullscreen] = newValue == .always - Defaults[.enableFullscreenMediaDetection] = newValue != .never + .onChange(of: hideNotchOption) { + Defaults[.enableFullscreenMediaDetection] = hideNotchOption != .never } } .navigationTitle("Media") } - + // Only show controller options that are available on this macOS version private var availableMediaControllers: [MediaControllerType] { if MusicManager.shared.isNowPlayingDeprecated { @@ -558,7 +560,7 @@ struct Media: View { } struct CalendarSettings: View { - @ObservedObject private var calendarManager = CalendarManager() + @ObservedObject private var calendarManager = CalendarManager.shared @Default(.showCalendar) var showCalendar: Bool var body: some View { @@ -574,7 +576,7 @@ struct CalendarSettings: View { } } } else { - Toggle("Show calendar", isOn: $showCalendar) + Defaults.Toggle("Show calendar", key: .showCalendar) Section(header: Text("Select Calendars")) { List { ForEach(calendarManager.allCalendars, id: \.id) { calendar in @@ -588,6 +590,7 @@ struct CalendarSettings: View { )) { Text(calendar.title) } + .disabled(!showCalendar) } } } @@ -603,157 +606,6 @@ struct CalendarSettings: View { } } -struct TimerSettings: View { - @ObservedObject private var coordinator = DynamicIslandViewCoordinator.shared - @Default(.enableTimerFeature) var enableTimerFeature - @AppStorage("customTimerDuration") private var customTimerDuration: Double = 600 // 10 minutes default - @State private var customMinutes: Int = 10 - @State private var customSeconds: Int = 0 - - var body: some View { - Form { - Section { - Defaults.Toggle("Enable timer feature", key: .enableTimerFeature) - - if enableTimerFeature { - Toggle( - "Enable timer live activity", - isOn: $coordinator.timerLiveActivityEnabled - ) - .animation(.easeInOut, value: coordinator.timerLiveActivityEnabled) - } - } header: { - Text("Timer Feature") - } footer: { - Text("Enable or disable the timer functionality in the Dynamic Island. The live activity toggle controls whether timer progress is shown in the expanded view.") - } - - if enableTimerFeature { - Section { - VStack(alignment: .leading, spacing: 16) { - Text("Custom Timer Duration") - .font(.headline) - - HStack(spacing: 16) { - VStack { - Text("Minutes") - .font(.system(size: 14, weight: .medium)) - .foregroundStyle(.secondary) - - Picker(selection: $customMinutes, label: Text("Minutes")) { - ForEach(0...59, id: \.self) { minute in - Text("\(minute)").tag(minute) - } - } - .pickerStyle(.menu) - .frame(width: 80, height: 40) - } - - VStack { - Text("Seconds") - .font(.system(size: 14, weight: .medium)) - .foregroundStyle(.secondary) - - Picker(selection: $customSeconds, label: Text("Seconds")) { - ForEach(0...59, id: \.self) { second in - Text("\(second)").tag(second) - } - } - .pickerStyle(.menu) - .frame(width: 80, height: 40) - } - } - - HStack { - Text("Current custom timer: \(customTimerDisplayText)") - .foregroundStyle(.secondary) - Spacer() - Button("Update") { - customTimerDuration = Double(customMinutes * 60 + customSeconds) - } - .disabled(customMinutes == 0 && customSeconds == 0) - } - } - } header: { - Text("Custom Timer") - } footer: { - Text("Set a custom duration for the timer. This will be used when you press the 'Custom' button in the timer interface.") - } - - Section { - VStack(alignment: .leading, spacing: 8) { - HStack { - Text("Timer Sound") - .font(.system(size: 16, weight: .medium)) - Spacer() - Button("Choose File") { - selectCustomTimerSound() - } - .buttonStyle(.bordered) - } - - if let customTimerSoundPath = UserDefaults.standard.string(forKey: "customTimerSoundPath") { - Text("Custom: \(URL(fileURLWithPath: customTimerSoundPath).lastPathComponent)") - .font(.caption) - .foregroundColor(.secondary) - } else { - Text("Default: dynamic.m4a") - .font(.caption) - .foregroundColor(.secondary) - } - - Button("Reset to Default") { - UserDefaults.standard.removeObject(forKey: "customTimerSoundPath") - } - .buttonStyle(.bordered) - .disabled(UserDefaults.standard.string(forKey: "customTimerSoundPath") == nil) - } - } header: { - Text("Timer Sound") - } footer: { - Text("Choose a custom sound file that will play when the timer completes. Supported formats: MP3, M4A, WAV, AIFF.") - } - } - } - .navigationTitle("Timer") - .onAppear { - let totalMinutes = Int(customTimerDuration) / 60 - customMinutes = totalMinutes - customSeconds = Int(customTimerDuration) % 60 - } - } - - private var customTimerDisplayText: String { - let totalMinutes = Int(customTimerDuration) / 60 - let seconds = Int(customTimerDuration) % 60 - let hours = totalMinutes / 60 - let minutes = totalMinutes % 60 - - if hours > 0 { - return "\(hours):\(String(format: "%02d", minutes)):\(String(format: "%02d", seconds))" - } else if minutes > 0 { - return "\(minutes):\(String(format: "%02d", seconds))" - } else { - return "\(seconds)s" - } - } - - private func selectCustomTimerSound() { - let panel = NSOpenPanel() - panel.title = "Select Timer Sound" - panel.allowedContentTypes = [.audio] - panel.allowsMultipleSelection = false - panel.canChooseDirectories = false - panel.canChooseFiles = true - - if panel.runModal() == .OK { - if let url = panel.url { - UserDefaults.standard.set(url.path, forKey: "customTimerSoundPath") - } - } - } -} - struct About: View { @State private var showBuildNumber: Bool = false let updaterController: SPUStandardUpdaterController @@ -786,9 +638,9 @@ struct About: View { } header: { Text("Version info") } - + UpdaterSettingsView(updater: updaterController.updater) - + HStack(spacing: 30) { Spacer(minLength: 0) Button { @@ -891,15 +743,15 @@ struct Extensions: View { .font(.footnote) } .frame(width: 60, alignment: .leading) - + Menu(content: { Button("Restart") { let ws = NSWorkspace.shared - + if let ext = ws.runningApplications.first(where: { $0.bundleIdentifier == item.bundleIdentifier }) { ext.terminate() } - + if let appURL = ws.urlForApplication(withBundleIdentifier: item.bundleIdentifier) { ws.openApplication(at: appURL, configuration: .init(), completionHandler: nil) } @@ -987,7 +839,7 @@ struct Appearance: View { let icons: [String] = ["logo2"] @State private var selectedIcon: String = "logo2" @State private var selectedListVisualizer: CustomVisualizer? = nil - + @State private var isPresented: Bool = false @State private var name: String = "" @State private var url: String = "" @@ -1003,7 +855,7 @@ struct Appearance: View { } header: { Text("General") } - + Section { Defaults.Toggle("Enable colored spectrograms", key: .coloredSpectrogram) Defaults @@ -1017,7 +869,7 @@ struct Appearance: View { } header: { Text("Media") } - + Section { Toggle( "Use music visualizer spectrogram", @@ -1053,7 +905,7 @@ struct Appearance: View { customBadge(text: "Coming soon") } } - + Section { List { ForEach(customVisualizers, id: \.self) { visualizer in @@ -1151,7 +1003,7 @@ struct Appearance: View { Text("Cancel") .frame(maxWidth: .infinity, alignment: .center) } - + Button { let visualizer: CustomVisualizer = .init( UUID: UUID(), @@ -1159,11 +1011,11 @@ struct Appearance: View { url: URL(string: url)!, speed: speed ) - + if !customVisualizers.contains(visualizer) { customVisualizers.append(visualizer) } - + isPresented.toggle() } label: { Text("Add") @@ -1185,7 +1037,7 @@ struct Appearance: View { } } } - + Section { Defaults.Toggle("Enable boring mirror", key: .showMirror) .disabled(!checkVideoInput()) @@ -1201,7 +1053,7 @@ struct Appearance: View { Text("Additional features") } } - + Section { HStack { ForEach(icons, id: \.self) { icon in @@ -1217,7 +1069,7 @@ struct Appearance: View { lineWidth: 2.5 ) ) - + Text("Default") .fontWeight(.medium) .font(.caption) @@ -1248,12 +1100,12 @@ struct Appearance: View { } .navigationTitle("Appearance") } - + func checkVideoInput() -> Bool { if let _ = AVCaptureDevice.default(for: .video) { return true } - + return false } } @@ -1274,190 +1126,11 @@ struct Shortcuts: View { Section { KeyboardShortcuts.Recorder("Toggle Notch Open:", name: .toggleNotchOpen) } - Section { - KeyboardShortcuts.Recorder("Start Demo Timer:", name: .startDemoTimer) - } header: { - Text("Timer") - } footer: { - Text("Starts a 5-minute demo timer to test the timer live activity feature.") - .multilineTextAlignment(.trailing) - .foregroundStyle(.secondary) - .font(.caption) - } } .navigationTitle("Shortcuts") } } -struct Stats: View { - @ObservedObject var statsManager = StatsManager.shared - @Default(.enableStatsFeature) var enableStatsFeature - @Default(.showCpuGraph) var showCpuGraph - @Default(.showMemoryGraph) var showMemoryGraph - @Default(.showGpuGraph) var showGpuGraph - @Default(.showNetworkGraph) var showNetworkGraph - @Default(.showDiskGraph) var showDiskGraph - - var enabledGraphsCount: Int { - [showCpuGraph, showMemoryGraph, showGpuGraph, showNetworkGraph, showDiskGraph].filter { $0 }.count - } - - var body: some View { - Form { - Section { - Defaults.Toggle("Enable system stats monitoring", key: .enableStatsFeature) - .onChange(of: enableStatsFeature) { _, newValue in - if newValue { - statsManager.startMonitoring() - } else { - statsManager.stopMonitoring() - } - } - } header: { - Text("General") - } footer: { - Text("When enabled, the Stats tab will display real-time system performance graphs. This feature requires system permissions and may use additional battery.") - .multilineTextAlignment(.trailing) - .foregroundStyle(.secondary) - .font(.caption) - } - - if enableStatsFeature { - Section { - Defaults.Toggle("CPU Usage", key: .showCpuGraph) - Defaults.Toggle("Memory Usage", key: .showMemoryGraph) - Defaults.Toggle("GPU Usage", key: .showGpuGraph) - Defaults.Toggle("Network Activity", key: .showNetworkGraph) - Defaults.Toggle("Disk I/O", key: .showDiskGraph) - } header: { - Text("Graph Visibility") - } footer: { - if enabledGraphsCount >= 4 { - Text("With \(enabledGraphsCount) graphs enabled, the Dynamic Island will expand horizontally to accommodate all graphs in a single row.") - .multilineTextAlignment(.trailing) - .foregroundStyle(.secondary) - .font(.caption) - } else { - Text("Each graph can be individually enabled or disabled. Network activity shows download/upload speeds, and disk I/O shows read/write speeds.") - .multilineTextAlignment(.trailing) - .foregroundStyle(.secondary) - .font(.caption) - } - } - - Section { - HStack { - Text("Monitoring Status") - Spacer() - HStack(spacing: 4) { - Circle() - .fill(statsManager.isMonitoring ? .green : .red) - .frame(width: 8, height: 8) - Text(statsManager.isMonitoring ? "Active" : "Stopped") - .foregroundStyle(.secondary) - } - } - - if statsManager.isMonitoring { - if showCpuGraph { - HStack { - Text("CPU Usage") - Spacer() - Text(statsManager.cpuUsageString) - .foregroundStyle(.secondary) - } - } - - if showMemoryGraph { - HStack { - Text("Memory Usage") - Spacer() - Text(statsManager.memoryUsageString) - .foregroundStyle(.secondary) - } - } - - if showGpuGraph { - HStack { - Text("GPU Usage") - Spacer() - Text(statsManager.gpuUsageString) - .foregroundStyle(.secondary) - } - } - - if showNetworkGraph { - HStack { - Text("Network Download") - Spacer() - Text(String(format: "%.1f MB/s", statsManager.networkDownload)) - .foregroundStyle(.secondary) - } - - HStack { - Text("Network Upload") - Spacer() - Text(String(format: "%.1f MB/s", statsManager.networkUpload)) - .foregroundStyle(.secondary) - } - } - - if showDiskGraph { - HStack { - Text("Disk Read") - Spacer() - Text(String(format: "%.1f MB/s", statsManager.diskRead)) - .foregroundStyle(.secondary) - } - - HStack { - Text("Disk Write") - Spacer() - Text(String(format: "%.1f MB/s", statsManager.diskWrite)) - .foregroundStyle(.secondary) - } - } - - HStack { - Text("Last Updated") - Spacer() - Text(statsManager.lastUpdated, style: .relative) - .foregroundStyle(.secondary) - } - } - } header: { - Text("Live Performance Data") - } - - Section { - HStack { - Button(statsManager.isMonitoring ? "Stop Monitoring" : "Start Monitoring") { - if statsManager.isMonitoring { - statsManager.stopMonitoring() - } else { - statsManager.startMonitoring() - } - } - .buttonStyle(.borderedProminent) - .foregroundColor(statsManager.isMonitoring ? .red : .blue) - - Spacer() - - Button("Clear Data") { - statsManager.clearHistory() - } - .buttonStyle(.bordered) - .disabled(statsManager.isMonitoring) - } - } header: { - Text("Controls") - } - } - } - .navigationTitle("Stats") - } -} - func proFeatureBadge() -> some View { Text("Upgrade to Pro") .foregroundStyle(Color(red: 0.545, green: 0.196, blue: 0.98)) @@ -1504,129 +1177,120 @@ func warningBadge(_ text: String, _ description: String) -> some View { } } -struct ClipboardSettings: View { - @ObservedObject var clipboardManager = ClipboardManager.shared - @Default(.enableClipboardManager) var enableClipboardManager - @Default(.clipboardHistorySize) var clipboardHistorySize - @Default(.showClipboardIcon) var showClipboardIcon - +struct TimerSettings: View { var body: some View { Form { Section { - Defaults.Toggle("Enable Clipboard Manager", key: .enableClipboardManager) - .onChange(of: enableClipboardManager) { enabled in - if enabled { - clipboardManager.startMonitoring() + Defaults.Toggle("Enable timer feature", key: .enableTimerFeature) + .onChange(of: Defaults[.enableTimerFeature]) { _, newValue in + if !newValue { + // Stop any running timers when feature is disabled + TimerManager.shared.stopTimer() + } + } + } header: { + Text("Timer") + } footer: { + if Defaults[.enableTimerFeature] { + Text("Timer tab will appear in the notch when enabled.") + } else { + Text("Enable to access timer functionality in the Dynamic Island.") + } + } + } + .navigationTitle("Timer") + } +} + +struct StatsSettings: View { + var body: some View { + Form { + Section { + Defaults.Toggle("Enable stats feature", key: .enableStatsFeature) + .onChange(of: Defaults[.enableStatsFeature]) { _, newValue in + if !newValue { + // Stop monitoring when feature is disabled + StatsManager.shared.stopMonitoring() + } + } + } header: { + Text("System Monitoring") + } footer: { + if Defaults[.enableStatsFeature] { + Text("Stats tab will appear in the notch when enabled.") + } else { + Text("Enable to monitor CPU, Memory, GPU, Network and Disk usage.") + } + } + + if Defaults[.enableStatsFeature] { + Section { + Defaults.Toggle("Show CPU graph", key: .showCpuGraph) + Defaults.Toggle("Show Memory graph", key: .showMemoryGraph) + Defaults.Toggle("Show GPU graph", key: .showGpuGraph) + Defaults.Toggle("Show Network graph", key: .showNetworkGraph) + Defaults.Toggle("Show Disk graph", key: .showDiskGraph) + } header: { + Text("Graph Visibility") + } footer: { + Text("Choose which performance graphs to display. 1-3 graphs use horizontal layout, 4+ graphs use compressed grid layout.") + } + } + } + .navigationTitle("Stats") + } +} + +struct ClipboardSettings: View { + var body: some View { + Form { + Section { + Defaults.Toggle("Enable clipboard manager", key: .enableClipboardManager) + .onChange(of: Defaults[.enableClipboardManager]) { _, newValue in + if newValue { + ClipboardManager.shared.startMonitoring() } else { - clipboardManager.stopMonitoring() + ClipboardManager.shared.stopMonitoring() } } } header: { Text("Clipboard Manager") } footer: { - Text("Monitor clipboard changes and keep a history of recent copies. Use Cmd+Shift+V to quickly access clipboard history.") + if Defaults[.enableClipboardManager] { + Text("Clipboard history will be tracked and accessible via the header button.") + } else { + Text("Enable to track clipboard history and access recent items.") + } } - if enableClipboardManager { + if Defaults[.enableClipboardManager] { Section { - Defaults.Toggle("Show Clipboard Icon", key: .showClipboardIcon) + Defaults.Toggle("Show clipboard icon", key: .showClipboardIcon) - HStack { - Text("History Size") - Spacer() - Picker("History Size", selection: $clipboardHistorySize) { - Text("3 items").tag(3) - Text("5 items").tag(5) - Text("7 items").tag(7) - Text("10 items").tag(10) + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("History size") + Spacer() + Text("\(Defaults[.clipboardHistorySize]) items") + .foregroundStyle(.secondary) } - .pickerStyle(.menu) - .frame(width: 100) - } - - HStack { - Text("Current Items") - Spacer() - Text("\(clipboardManager.clipboardHistory.count)") - .foregroundColor(.secondary) - } - - HStack { - Text("Monitoring Status") - Spacer() - Text(clipboardManager.isMonitoring ? "Active" : "Stopped") - .foregroundColor(clipboardManager.isMonitoring ? .green : .secondary) + Slider( + value: Binding( + get: { Double(Defaults[.clipboardHistorySize]) }, + set: { Defaults[.clipboardHistorySize] = Int($0) } + ), + in: 3...10, + step: 1 + ) } } header: { Text("Settings") } footer: { - Text("The clipboard icon appears in the header next to the settings gear when enabled. Use the global shortcut Cmd+Shift+V or click the icon to access history.") - } - - Section { - Button("Clear Clipboard History") { - clipboardManager.clearHistory() - } - .foregroundColor(.red) - .disabled(clipboardManager.clipboardHistory.isEmpty) - } header: { - Text("Actions") - } footer: { - Text("This will permanently delete all stored clipboard history. The clipboard button is located to the left of the settings gear in the header.") - } - - if !clipboardManager.clipboardHistory.isEmpty { - Section { - ForEach(clipboardManager.clipboardHistory) { item in - VStack(alignment: .leading, spacing: 4) { - HStack { - Image(systemName: item.type.icon) - .foregroundColor(.blue) - .frame(width: 16) - Text(item.type.displayName) - .font(.caption) - .foregroundColor(.secondary) - Spacer() - Text(timeAgoString(from: item.timestamp)) - .font(.caption2) - .foregroundColor(.secondary) - } - Text(item.preview) - .font(.system(.body, design: .monospaced)) - .lineLimit(2) - } - .padding(.vertical, 2) - } - } header: { - Text("Current History") - } + Text("Configure how many recent clipboard items to remember (minimum 3).") } } } - .formStyle(.grouped) .navigationTitle("Clipboard") - .onAppear { - if enableClipboardManager && !clipboardManager.isMonitoring { - clipboardManager.startMonitoring() - } - } - } - - private func timeAgoString(from date: Date) -> String { - let interval = Date().timeIntervalSince(date) - - if interval < 60 { - return "Just now" - } else if interval < 3600 { - let minutes = Int(interval / 60) - return "\(minutes)m ago" - } else if interval < 86400 { - let hours = Int(interval / 3600) - return "\(hours)h ago" - } else { - let days = Int(interval / 86400) - return "\(days)d ago" - } } } diff --git a/DynamicIsland/helpers/AppIcons.swift b/DynamicIsland/helpers/AppIcons.swift index 1123b01..5941fc0 100644 --- a/DynamicIsland/helpers/AppIcons.swift +++ b/DynamicIsland/helpers/AppIcons.swift @@ -26,13 +26,6 @@ struct AppIcons { return getIcon(file: path) } - func getIcon(application: String) -> NSImage? { - guard let path = NSWorkspace.shared.fullPath(forApplication: application) - else { return nil } - - return getIcon(file: path) - } - /// Easily read Info.plist as a Dictionary from any bundle by accessing .infoDictionary on Bundle func bundle(forBundleID: String) -> Bundle? { guard let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: forBundleID) diff --git a/DynamicIsland/helpers/MediaChecker.swift b/DynamicIsland/helpers/MediaChecker.swift new file mode 100644 index 0000000..04d0c20 --- /dev/null +++ b/DynamicIsland/helpers/MediaChecker.swift @@ -0,0 +1,67 @@ +// +// MediaChecker.swift +// DynamicIsland +// +// Created by Alexander on 2025-07-26. +// + +import Foundation + +@MainActor +final class MediaChecker: Sendable { + private(set) var isNowPlayingDeprecated: Bool = false + + enum MediaCheckerError: Error { + case missingResources + case processExecutionFailed + case timeout + } + + func checkDeprecationStatus() async throws -> Bool { + guard let scriptURL = Bundle.main.url(forResource: "mediaremote-adapter", withExtension: "pl"), + let nowPlayingTestClientPath = Bundle.main.url(forResource: "NowPlayingTestClient", withExtension: nil)?.path, + let frameworkPath = Bundle.main.privateFrameworksPath?.appending("/MediaRemoteAdapter.framework") + else { + throw MediaCheckerError.missingResources + } + + let process = Process() + process.executableURL = URL(fileURLWithPath: "/usr/bin/perl") + process.arguments = [scriptURL.path, frameworkPath, nowPlayingTestClientPath, "test"] + let pipe = Pipe() + process.standardOutput = pipe + + do { + try process.run() + } catch { + throw MediaCheckerError.processExecutionFailed + } + + // Timeout after 10 seconds + let result: String = try await withThrowingTaskGroup(of: String?.self) { group in + group.addTask { + process.waitUntilExit() + let data = pipe.fileHandleForReading.readDataToEndOfFile() + return String(data: data, encoding: .utf8) + } + group.addTask { + try await Task.sleep(for: .seconds(10)) + if process.isRunning { + process.terminate() + } + return "0" // Default value if process takes too long + } + for try await output in group { + if let output = output { + group.cancelAll() + return output + } + } + throw MediaCheckerError.timeout + } + + let isDeprecated = result.trimmingCharacters(in: .whitespacesAndNewlines).last == "1" + self.isNowPlayingDeprecated = isDeprecated + return isDeprecated + } +} diff --git a/DynamicIsland/managers/CalendarManager.swift b/DynamicIsland/managers/CalendarManager.swift index 7afc23e..a83906c 100644 --- a/DynamicIsland/managers/CalendarManager.swift +++ b/DynamicIsland/managers/CalendarManager.swift @@ -13,6 +13,8 @@ import Defaults @MainActor class CalendarManager: ObservableObject { + static let shared = CalendarManager() + @Published var currentWeekStartDate: Date @Published var events: [EventModel] = [] @Published var allCalendars: [CalendarModel] = [] @@ -20,7 +22,7 @@ class CalendarManager: ObservableObject { private var selectedCalendars: [CalendarModel] = [] private let calendarService = CalendarService() - init() { + private init() { self.currentWeekStartDate = CalendarManager.startOfDay(Date()) } @@ -33,7 +35,13 @@ class CalendarManager: ObservableObject { switch status { case .notDetermined: - self.authorizationStatus = await calendarService.requestAccess() ? .fullAccess : .denied + let granted = await calendarService.requestAccess() + self.authorizationStatus = granted ? .fullAccess : .denied + if granted { + allCalendars = await calendarService.calendars() + updateSelectedCalendars() + events = await calendarService.events(from: currentWeekStartDate, to: Calendar.current.date(byAdding: .day, value: 1, to: currentWeekStartDate)!, calendars: selectedCalendars.map{$0.id}) + } case .restricted, .denied: // Handle the case where the user has denied or restricted access NSLog("Calendar access denied or restricted") @@ -48,11 +56,11 @@ class CalendarManager: ObservableObject { print("Unknown authorization status") } } - + func updateSelectedCalendars() { selectedCalendars = allCalendars.filter { getCalendarSelected($0) } } - + func getCalendarSelected(_ calendar: CalendarModel) -> Bool { switch Defaults[.calendarSelectionState] { case .all: @@ -78,7 +86,7 @@ class CalendarManager: ObservableObject { } else { identifiers.remove(calendar.id) } - + selectionState = identifiers.isEmpty ? .all : identifiers.count == allCalendars.count ? .all : .selected(identifiers) // if empty, select all } @@ -86,11 +94,11 @@ class CalendarManager: ObservableObject { updateSelectedCalendars() events = await calendarService.events(from: currentWeekStartDate, to: Calendar.current.date(byAdding: .day, value: 1, to: currentWeekStartDate)!, calendars: selectedCalendars.map{$0.id}) } - + static func startOfDay(_ date: Date) -> Date { return Calendar.current.startOfDay(for: date) } - + func updateCurrentDate(_ date: Date) async { currentWeekStartDate = Calendar.current.startOfDay(for: date) events = await calendarService.events(from: currentWeekStartDate, to: Calendar.current.date(byAdding: .day, value: 1, to: currentWeekStartDate)!, calendars: selectedCalendars.map{$0.id}) diff --git a/DynamicIsland/managers/MusicManager.swift b/DynamicIsland/managers/MusicManager.swift index 9e7fc27..b7e9314 100644 --- a/DynamicIsland/managers/MusicManager.swift +++ b/DynamicIsland/managers/MusicManager.swift @@ -19,15 +19,11 @@ class MusicManager: ObservableObject { static let shared = MusicManager() private var cancellables = Set() private var controllerCancellables = Set() - private var debounceToggle: DispatchWorkItem? + private var debounceIdleTask: Task? - // Helper to check if macOS is too new for NowPlayingController - public var isNowPlayingDeprecated: Bool { - if #available(macOS 15.4, *) { - return true - } - return false - } + // Helper to check if macOS has removed support for NowPlayingController + public private(set) var isNowPlayingDeprecated: Bool = false + private let mediaChecker = MediaChecker() // Active controller private var activeController: (any MediaControllerProtocol)? @@ -48,6 +44,8 @@ class MusicManager: ObservableObject { @Published var elapsedTime: TimeInterval = 0 @Published var timestampDate: Date = .init() @Published var playbackRate: Double = 1 + @Published var isShuffled: Bool = false + @Published var repeatMode: RepeatMode = .off @ObservedObject var coordinator = DynamicIslandViewCoordinator.shared @Published var usingAppIconForArtwork: Bool = false @@ -67,13 +65,24 @@ class MusicManager: ObservableObject { self?.setActiveControllerBasedOnPreference() } .store(in: &cancellables) + + // Initialize deprecation check asynchronously + Task { @MainActor in + do { + self.isNowPlayingDeprecated = try await self.mediaChecker.checkDeprecationStatus() + print("Deprecation check completed: \(self.isNowPlayingDeprecated)") + } catch { + print("Failed to check deprecation status: \(error). Defaulting to false.") + self.isNowPlayingDeprecated = false + } - // Initialize the active controller - setActiveControllerBasedOnPreference() + // Initialize the active controller after deprecation check + self.setActiveControllerBasedOnPreference() + } } deinit { - debounceToggle?.cancel() + debounceIdleTask?.cancel() cancellables.removeAll() controllerCancellables.removeAll() flipWorkItem?.cancel() @@ -98,7 +107,17 @@ class MusicManager: ObservableObject { // Only create NowPlayingController if not deprecated on this macOS version if !self.isNowPlayingDeprecated { ignoreLastUpdated = false - newController = NowPlayingController() + if let controller = NowPlayingController() { + // Check if the controller can actually work + if controller.isWorking { + newController = controller + } else { + print("⚠️ NowPlayingController created but resources unavailable, falling back to default") + return nil + } + } else { + return nil + } } else { return nil } @@ -285,19 +304,20 @@ class MusicManager: ObservableObject { private func updateIdleState(state: Bool) { if state { isPlayerIdle = false - debounceToggle?.cancel() + debounceIdleTask?.cancel() } else { - debounceToggle = DispatchWorkItem { [weak self] in - guard let self = self else { return } - if self.lastUpdated.timeIntervalSinceNow < -Defaults[.waitInterval] { - withAnimation { - self.isPlayerIdle = !self.isPlaying + debounceIdleTask?.cancel() + debounceIdleTask = Task { [weak self] in + try? await Task.sleep(for: .seconds(Defaults[.waitInterval])) + guard let self = self, !Task.isCancelled else { return } + await MainActor.run { + if self.lastUpdated.timeIntervalSinceNow < -Defaults[.waitInterval] { + withAnimation { + self.isPlayerIdle = !self.isPlaying + } } } } - - DispatchQueue.main.asyncAfter( - deadline: .now() + Defaults[.waitInterval], execute: debounceToggle!) } } @@ -338,31 +358,57 @@ class MusicManager: ObservableObject { // MARK: - Public Methods for controlling playback func playPause() { - activeController?.togglePlay() + Task { + await activeController?.togglePlay() + } } func play() { - activeController?.play() + Task { + await activeController?.play() + } } func pause() { - activeController?.pause() + Task { + await activeController?.pause() + } + } + + func toggleShuffle() { + Task { + await activeController?.toggleShuffle() + } + } + + func toggleRepeat() { + Task { + await activeController?.toggleRepeat() + } } func togglePlay() { - activeController?.togglePlay() + Task { + await activeController?.togglePlay() + } } func nextTrack() { - activeController?.nextTrack() + Task { + await activeController?.nextTrack() + } } func previousTrack() { - activeController?.previousTrack() + Task { + await activeController?.previousTrack() + } } func seek(to position: TimeInterval) { - activeController?.seek(to: position) + Task { + await activeController?.seek(to: position) + } } func openMusicApp() { @@ -388,9 +434,14 @@ class MusicManager: ObservableObject { func forceUpdate() { // Request immediate update from the active controller - DispatchQueue.main.async { [weak self] in + Task { [weak self] in if self?.activeController?.isActive() == true { - self?.activeController?.updatePlaybackInfo() + if type(of: self?.activeController) == YouTubeMusicController.self, + let youtubeController = self?.activeController as? YouTubeMusicController { + await youtubeController.pollPlaybackState() + } else { + await self?.activeController?.updatePlaybackInfo() + } } } } diff --git a/DynamicIsland/managers/NotchSpaceManager.swift b/DynamicIsland/managers/NotchSpaceManager.swift index c781179..4954a79 100644 --- a/DynamicIsland/managers/NotchSpaceManager.swift +++ b/DynamicIsland/managers/NotchSpaceManager.swift @@ -2,7 +2,7 @@ // NotchSpaceManager.swift // DynamicIsland // -// Created by Alexander Greco on 2024-10-27. +// Created by Alexander on 2024-10-27. // import Foundation diff --git a/DynamicIsland/models/BatteryStatusViewModel.swift b/DynamicIsland/models/BatteryStatusViewModel.swift index f439248..f8e9872 100644 --- a/DynamicIsland/models/BatteryStatusViewModel.swift +++ b/DynamicIsland/models/BatteryStatusViewModel.swift @@ -1,12 +1,12 @@ -import Foundation import Cocoa -import SwiftUI -import IOKit.ps import Defaults +import Foundation +import IOKit.ps +import SwiftUI /// A view model that manages and monitors the battery status of the device class BatteryStatusViewModel: ObservableObject { - + private var wasCharging: Bool = false private var powerSourceChangedCallback: IOPowerSourceCallbackType? private var runLoopSource: Unmanaged? @@ -22,14 +22,14 @@ class BatteryStatusViewModel: ObservableObject { @Published private(set) var isInitial: Bool = false @Published private(set) var timeToFullCharge: Int = 0 @Published private(set) var statusText: String = "" - + private let managerBattery = BatteryActivityManager.shared private var managerBatteryId: Int? - + static let shared = BatteryStatusViewModel() - /// Initializes the view model with a given DynamicIslandViewModel instance - /// - Parameter vm: The DynamicIslandViewModel instance + /// Initializes the view model with a given BoringViewModel instance + /// - Parameter vm: The BoringViewModel instance private init() { setupPowerStatus() setupMonitor() @@ -48,58 +48,61 @@ class BatteryStatusViewModel: ObservableObject { self.handleBatteryEvent(event) } } - + /// Handles battery events and updates the corresponding properties /// - Parameter event: The battery event to handle - private func handleBatteryEvent(_ event: BatteryActivityManager.BatteryEvent) { + private func handleBatteryEvent(_ event: BatteryActivityManager.BatteryEvent) { switch event { - case .powerSourceChanged(let isPluggedIn): - print("🔌 Power source: \(isPluggedIn ? "Connected" : "Disconnected")") - withAnimation { - self.isPluggedIn = isPluggedIn - self.statusText = isPluggedIn ? "Plugged In" : "Unplugged" - self.notifyImportanChangeStatus() - } - - case .batteryLevelChanged(let level): - print("🔋 Battery level: \(Int(level))%") - withAnimation { - self.levelBattery = level - } - - case .lowPowerModeChanged(let isEnabled): - print("⚡ Low power mode: \(isEnabled ? "Enabled" : "Disabled")") - self.notifyImportanChangeStatus() - withAnimation { - self.isInLowPowerMode = isEnabled - self.statusText = "Low Power: \(self.isInLowPowerMode ? "On" : "Off")" - } - - case .isChargingChanged(let isCharging): - print("🔌 Charging: \(isCharging ? "Yes" : "No")") - print("maxCapacity: \(self.maxCapacity)") - print("levelBattery: \(self.levelBattery)") - self.notifyImportanChangeStatus() - withAnimation { - self.isCharging = isCharging - self.statusText = isCharging ? "Charging battery" : (self.levelBattery < self.maxCapacity ? "Not charging" : "Full charge") - } - - case .timeToFullChargeChanged(let time): - print("🕒 Time to full charge: \(time) minutes") - withAnimation { - self.timeToFullCharge = time - } - - case .maxCapacityChanged(let capacity): - print("🔋 Max capacity: \(capacity)") - withAnimation { - self.maxCapacity = capacity - } - - case .error(let description): - print("⚠️ Error: \(description)") + case .powerSourceChanged(let isPluggedIn): + print("🔌 Power source: \(isPluggedIn ? "Connected" : "Disconnected")") + withAnimation { + self.isPluggedIn = isPluggedIn + self.statusText = isPluggedIn ? "Plugged In" : "Unplugged" + self.notifyImportanChangeStatus() } + + case .batteryLevelChanged(let level): + print("🔋 Battery level: \(Int(level))%") + withAnimation { + self.levelBattery = level + } + + case .lowPowerModeChanged(let isEnabled): + print("⚡ Low power mode: \(isEnabled ? "Enabled" : "Disabled")") + self.notifyImportanChangeStatus() + withAnimation { + self.isInLowPowerMode = isEnabled + self.statusText = "Low Power: \(self.isInLowPowerMode ? "On" : "Off")" + } + + case .isChargingChanged(let isCharging): + print("🔌 Charging: \(isCharging ? "Yes" : "No")") + print("maxCapacity: \(self.maxCapacity)") + print("levelBattery: \(self.levelBattery)") + self.notifyImportanChangeStatus() + withAnimation { + self.isCharging = isCharging + self.statusText = + isCharging + ? "Charging battery" + : (self.levelBattery < self.maxCapacity ? "Not charging" : "Full charge") + } + + case .timeToFullChargeChanged(let time): + print("🕒 Time to full charge: \(time) minutes") + withAnimation { + self.timeToFullCharge = time + } + + case .maxCapacityChanged(let capacity): + print("🔋 Max capacity: \(capacity)") + withAnimation { + self.maxCapacity = capacity + } + + case .error(let description): + print("⚠️ Error: \(description)") + } } /// Updates the battery information with the given BatteryInfo instance @@ -114,25 +117,13 @@ class BatteryStatusViewModel: ObservableObject { self.maxCapacity = batteryInfo.maxCapacity self.statusText = batteryInfo.isPluggedIn ? "Plugged In" : "Unplugged" } - notifyImportanChangeStatus(delay: coordinator.firstLaunch ? 6 : 0.0) - withAnimation { - if self.isCharging { - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - self.statusText = "Charging: Yes" - } - } - if self.isInLowPowerMode { - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - self.statusText = "Low Power: On" - } - } - } } - + /// Notifies important changes in the battery status with an optional delay /// - Parameter delay: The delay before notifying the change, default is 0.0 private func notifyImportanChangeStatus(delay: Double = 0.0) { - DispatchQueue.main.asyncAfter(deadline: .now() + delay) { + Task { + try? await Task.sleep(for: .seconds(delay)) self.coordinator.toggleExpandingView(status: true, type: .battery) } } @@ -143,5 +134,5 @@ class BatteryStatusViewModel: ObservableObject { managerBattery.removeObserver(byId: managerBatteryId) } } - + } diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index 7a67952..0cb1b52 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -117,6 +117,7 @@ extension Defaults.Keys { static let sneakPeekStyles = Key("sneakPeekStyles", default: .standard) static let enableFullscreenMediaDetection = Key("enableFullscreenMediaDetection", default: true) static let waitInterval = Key("waitInterval", default: 3) + static let showShuffleAndRepeat = Key("showShuffleAndRepeat", default: false) // MARK: Battery static let showPowerStatusNotifications = Key("showPowerStatusNotifications", default: true) diff --git a/DynamicIsland/models/DynamicIslandViewModel.swift b/DynamicIsland/models/DynamicIslandViewModel.swift index a55cf2c..54c64f4 100644 --- a/DynamicIsland/models/DynamicIslandViewModel.swift +++ b/DynamicIsland/models/DynamicIslandViewModel.swift @@ -30,6 +30,10 @@ class DynamicIslandViewModel: NSObject, ObservableObject { @Published var isHoveringCalendar: Bool = false @Published var isBatteryPopoverActive: Bool = false @Published var isClipboardPopoverActive: Bool = false + + let webcamManager = WebcamManager.shared + @Published var isCameraExpanded: Bool = false + @Published var isRequestingAuthorization: Bool = false @Published var screen: String? @@ -172,4 +176,46 @@ class DynamicIslandViewModel: NSObject, ObservableObject { } } } + + func toggleCameraPreview() { + if isRequestingAuthorization { + return + } + + switch webcamManager.authorizationStatus { + case .authorized: + if webcamManager.isSessionRunning { + webcamManager.stopSession() + isCameraExpanded = false + } else if webcamManager.cameraAvailable { + webcamManager.startSession() + isCameraExpanded = true + } + + 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." + alert.addButton(withTitle: "OK") + alert.runModal() + + NSApp.setActivationPolicy(.accessory) + NSApp.deactivate() + } + + case .notDetermined: + isRequestingAuthorization = true + webcamManager.checkAndRequestVideoAuthorization() + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + self.isRequestingAuthorization = false + } + + default: + break + } + } } diff --git a/DynamicIsland/models/PlaybackState.swift b/DynamicIsland/models/PlaybackState.swift index d758cf7..baa3501 100644 --- a/DynamicIsland/models/PlaybackState.swift +++ b/DynamicIsland/models/PlaybackState.swift @@ -7,6 +7,12 @@ import Foundation +enum RepeatMode: Int, Codable { + case off = 1 + case one = 2 + case all = 3 +} + struct PlaybackState { var bundleIdentifier: String var isPlaying: Bool = false @@ -16,8 +22,8 @@ struct PlaybackState { var currentTime: Double = 0 var duration: Double = 0 var playbackRate: Double = 1 - var isShuffled: Bool? = nil - var isRepeating: Bool? = nil + var isShuffled: Bool = false + var repeatMode: RepeatMode = .off var lastUpdated: Date = Date.distantPast var artwork: Data? } diff --git a/DynamicIsland/observers/FullscreenMediaDetection.swift b/DynamicIsland/observers/FullscreenMediaDetection.swift index 3541758..abbc5a8 100644 --- a/DynamicIsland/observers/FullscreenMediaDetection.swift +++ b/DynamicIsland/observers/FullscreenMediaDetection.swift @@ -5,9 +5,6 @@ // Created by Richard Kunkli on 06/09/2024. // -import Accessibility -import Cocoa -import CoreAudio import Defaults import MacroVisionKit import SwiftUI @@ -16,15 +13,15 @@ import SwiftUI class FullscreenMediaDetector: ObservableObject { static let shared = FullscreenMediaDetector() private let detector: MacroVisionKit + @ObservedObject private var musicManager = MusicManager.shared - // changed from [NSScreen:Bool] to [screenName:Bool] @Published private(set) var fullscreenStatus: [String: Bool] = [:] private init() { self.detector = MacroVisionKit.shared detector.configuration.includeSystemApps = true setupNotificationObservers() - updateFullScreenStatus() // now on MainActor + updateFullScreenStatus() } private func setupNotificationObservers() { @@ -42,25 +39,20 @@ class FullscreenMediaDetector: ObservableObject { } private func updateFullScreenStatus() { - NSLog("🔄 Fullscreen status update triggered") guard Defaults[.enableFullscreenMediaDetection] else { - NSLog("❌ Fullscreen media detection disabled") - let reset = Dictionary(uniqueKeysWithValues: NSScreen.screens.map { ($0.localizedName, false) } - ) + let reset = Dictionary(uniqueKeysWithValues: NSScreen.screens.map { ($0.localizedName, false) }) if reset != fullscreenStatus { fullscreenStatus = reset - NSLog("⏸ Fullscreen disabled → reset all screens") } return } - NSLog("🔄 Fullscreen media detection enabled") let apps = detector.detectFullscreenApps(debug: false) let names = NSScreen.screens.map { $0.localizedName } var newStatus: [String: Bool] = [:] for name in names { - newStatus[name] = apps.contains { $0.screen.localizedName == name && $0.bundleIdentifier != "com.apple.finder" } + newStatus[name] = apps.contains { $0.screen.localizedName == name && $0.bundleIdentifier != "com.apple.finder" && ($0.bundleIdentifier == musicManager.bundleIdentifier || Defaults[.hideNotchOption] == .always) } } if newStatus != fullscreenStatus { @@ -69,15 +61,6 @@ class FullscreenMediaDetector: ObservableObject { } } - func isScreenInFullScreen(_ screen: NSScreen) -> Bool { - let screenName = screen.localizedName - return fullscreenStatus[screenName] ?? false - } - - func isAnyScreenInFullScreen() -> Bool { - return fullscreenStatus.values.contains(true) - } - private func cleanupNotificationObservers() { NSWorkspace.shared.notificationCenter.removeObserver(self) } diff --git a/DynamicIsland/sizing/matters.swift b/DynamicIsland/sizing/matters.swift index f943f85..9d169b2 100644 --- a/DynamicIsland/sizing/matters.swift +++ b/DynamicIsland/sizing/matters.swift @@ -1,7 +1,9 @@ // -// sizeMatters.swift +// +// matters.swift // DynamicIsland // +// // Created by Harsh Vardhan Goswami on 05/08/24. // @@ -12,7 +14,7 @@ import SwiftUI let downloadSneakSize: CGSize = .init(width: 65, height: 1) let batterySneakSize: CGSize = .init(width: 160, height: 1) -let openNotchSize: CGSize = .init(width: 610, height: 200) +let openNotchSize: CGSize = .init(width: 640, height: 190) let cornerRadiusInsets: (opened: (top: CGFloat, bottom: CGFloat), closed: (top: CGFloat, bottom: CGFloat)) = (opened: (top: 19, bottom: 24), closed: (top: 6, bottom: 14)) enum MusicPlayerImageSizes { diff --git a/Frameworks/MediaRemoteAdapter.framework/MediaRemoteAdapter b/Frameworks/MediaRemoteAdapter.framework/MediaRemoteAdapter new file mode 120000 index 0000000..65b3513 --- /dev/null +++ b/Frameworks/MediaRemoteAdapter.framework/MediaRemoteAdapter @@ -0,0 +1 @@ +Versions/Current/MediaRemoteAdapter \ No newline at end of file diff --git a/Frameworks/MediaRemoteAdapter.framework/Resources b/Frameworks/MediaRemoteAdapter.framework/Resources new file mode 120000 index 0000000..953ee36 --- /dev/null +++ b/Frameworks/MediaRemoteAdapter.framework/Resources @@ -0,0 +1 @@ +Versions/Current/Resources \ No newline at end of file diff --git a/Frameworks/MediaRemoteAdapter.framework/Versions/A/MediaRemoteAdapter b/Frameworks/MediaRemoteAdapter.framework/Versions/A/MediaRemoteAdapter new file mode 100755 index 0000000000000000000000000000000000000000..05db77de2557a6a56d73215a610e634913508d61 GIT binary patch literal 462128 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%0AxqP8?L+?M=Emga{Oy+Jl*4e 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 L2oMDRj}iDU^qmWW literal 0 HcmV?d00001 diff --git a/Frameworks/MediaRemoteAdapter.framework/Versions/A/Resources/Info.plist b/Frameworks/MediaRemoteAdapter.framework/Versions/A/Resources/Info.plist new file mode 100644 index 0000000..bf2b7b1 --- /dev/null +++ b/Frameworks/MediaRemoteAdapter.framework/Versions/A/Resources/Info.plist @@ -0,0 +1,28 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + MediaRemoteAdapter + CFBundleIconFile + + CFBundleIdentifier + com.vandenbe.MediaRemoteAdapter + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + MediaRemoteAdapter + CFBundlePackageType + FMWK + CFBundleSignature + ???? + CFBundleVersion + 0.1.0 + CFBundleShortVersionString + 0.1 + CSResourcesFileMapped + + + diff --git a/Frameworks/MediaRemoteAdapter.framework/Versions/A/_CodeSignature/CodeResources b/Frameworks/MediaRemoteAdapter.framework/Versions/A/_CodeSignature/CodeResources new file mode 100644 index 0000000..16ea63b --- /dev/null +++ b/Frameworks/MediaRemoteAdapter.framework/Versions/A/_CodeSignature/CodeResources @@ -0,0 +1,128 @@ + + + + + files + + Resources/Info.plist + + M6AF1VWVJ1A/DSliCSjg170FqsY= + + + files2 + + Resources/Info.plist + + hash2 + + z3yWmTAqjdrPJEZUQ+t6AVPhw0e/I8PAiVr0HIU2ivg= + + + + rules + + ^Resources/ + + ^Resources/.*\.lproj/ + + optional + + weight + 1000 + + ^Resources/.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Resources/Base\.lproj/ + + weight + 1010 + + ^version.plist$ + + + rules2 + + .*\.dSYM($|/) + + weight + 11 + + ^(.*/)?\.DS_Store$ + + omit + + weight + 2000 + + ^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/ + + nested + + weight + 10 + + ^.* + + ^Info\.plist$ + + omit + + weight + 20 + + ^PkgInfo$ + + omit + + weight + 20 + + ^Resources/ + + weight + 20 + + ^Resources/.*\.lproj/ + + optional + + weight + 1000 + + ^Resources/.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Resources/Base\.lproj/ + + weight + 1010 + + ^[^/]+$ + + nested + + weight + 10 + + ^embedded\.provisionprofile$ + + weight + 20 + + ^version\.plist$ + + weight + 20 + + + + diff --git a/Frameworks/MediaRemoteAdapter.framework/Versions/Current b/Frameworks/MediaRemoteAdapter.framework/Versions/Current new file mode 120000 index 0000000..8c7e5a6 --- /dev/null +++ b/Frameworks/MediaRemoteAdapter.framework/Versions/Current @@ -0,0 +1 @@ +A \ No newline at end of file diff --git a/mediaremote-adapter/MediaRemoteAdapter.framework/MediaRemoteAdapter b/mediaremote-adapter/MediaRemoteAdapter.framework/MediaRemoteAdapter new file mode 120000 index 0000000..65b3513 --- /dev/null +++ b/mediaremote-adapter/MediaRemoteAdapter.framework/MediaRemoteAdapter @@ -0,0 +1 @@ +Versions/Current/MediaRemoteAdapter \ No newline at end of file diff --git a/mediaremote-adapter/MediaRemoteAdapter.framework/Resources b/mediaremote-adapter/MediaRemoteAdapter.framework/Resources new file mode 120000 index 0000000..953ee36 --- /dev/null +++ b/mediaremote-adapter/MediaRemoteAdapter.framework/Resources @@ -0,0 +1 @@ +Versions/Current/Resources \ No newline at end of file diff --git a/mediaremote-adapter/MediaRemoteAdapter.framework/Versions/A/MediaRemoteAdapter b/mediaremote-adapter/MediaRemoteAdapter.framework/Versions/A/MediaRemoteAdapter new file mode 100755 index 0000000000000000000000000000000000000000..05db77de2557a6a56d73215a610e634913508d61 GIT binary patch literal 462128 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%0AxqP8?L+?M=Emga{Oy+Jl*4e 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 L2oMDRj}iDU^qmWW literal 0 HcmV?d00001 diff --git a/mediaremote-adapter/MediaRemoteAdapter.framework/Versions/A/Resources/Info.plist b/mediaremote-adapter/MediaRemoteAdapter.framework/Versions/A/Resources/Info.plist new file mode 100644 index 0000000..bf2b7b1 --- /dev/null +++ b/mediaremote-adapter/MediaRemoteAdapter.framework/Versions/A/Resources/Info.plist @@ -0,0 +1,28 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + MediaRemoteAdapter + CFBundleIconFile + + CFBundleIdentifier + com.vandenbe.MediaRemoteAdapter + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + MediaRemoteAdapter + CFBundlePackageType + FMWK + CFBundleSignature + ???? + CFBundleVersion + 0.1.0 + CFBundleShortVersionString + 0.1 + CSResourcesFileMapped + + + diff --git a/mediaremote-adapter/MediaRemoteAdapter.framework/Versions/A/_CodeSignature/CodeResources b/mediaremote-adapter/MediaRemoteAdapter.framework/Versions/A/_CodeSignature/CodeResources new file mode 100644 index 0000000..16ea63b --- /dev/null +++ b/mediaremote-adapter/MediaRemoteAdapter.framework/Versions/A/_CodeSignature/CodeResources @@ -0,0 +1,128 @@ + + + + + files + + Resources/Info.plist + + M6AF1VWVJ1A/DSliCSjg170FqsY= + + + files2 + + Resources/Info.plist + + hash2 + + z3yWmTAqjdrPJEZUQ+t6AVPhw0e/I8PAiVr0HIU2ivg= + + + + rules + + ^Resources/ + + ^Resources/.*\.lproj/ + + optional + + weight + 1000 + + ^Resources/.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Resources/Base\.lproj/ + + weight + 1010 + + ^version.plist$ + + + rules2 + + .*\.dSYM($|/) + + weight + 11 + + ^(.*/)?\.DS_Store$ + + omit + + weight + 2000 + + ^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/ + + nested + + weight + 10 + + ^.* + + ^Info\.plist$ + + omit + + weight + 20 + + ^PkgInfo$ + + omit + + weight + 20 + + ^Resources/ + + weight + 20 + + ^Resources/.*\.lproj/ + + optional + + weight + 1000 + + ^Resources/.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Resources/Base\.lproj/ + + weight + 1010 + + ^[^/]+$ + + nested + + weight + 10 + + ^embedded\.provisionprofile$ + + weight + 20 + + ^version\.plist$ + + weight + 20 + + + + diff --git a/mediaremote-adapter/MediaRemoteAdapter.framework/Versions/Current b/mediaremote-adapter/MediaRemoteAdapter.framework/Versions/Current new file mode 120000 index 0000000..8c7e5a6 --- /dev/null +++ b/mediaremote-adapter/MediaRemoteAdapter.framework/Versions/Current @@ -0,0 +1 @@ +A \ No newline at end of file diff --git a/mediaremote-adapter/NowPlayingTestClient b/mediaremote-adapter/NowPlayingTestClient new file mode 100755 index 0000000000000000000000000000000000000000..750e43be2a796076b964c8c71cfb52ea895dd277 GIT binary patch literal 77848 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 "durationMicros" + "elapsedTime" -> "elapsedTimeMicros" + "timestamp" -> "timestampEpochMicros" (converted to epoch time) + --human-readable, -h: Makes values human-readable. Use only for debugging. + The JSON output is pretty-printed and the following keys are adapted: + "artworkData" -> Binary data is truncated to a shorter representation + +Examples (script name and framework path omitted): + stream --no-diff --debounce=100 + send 2 # Toggles play/pause in the media player (kMRATogglePlayPause) + repeat 3 # Sets the repeat mode to "playlist" (kMRARepeatModePlaylist) + +HELP + exit 0; +} + +if (!defined $ARGV[1]) { + print_help(); +} + +sub fail { + my ($error) = @_; + print STDERR "$error\n"; + exit 1; +} + +fail "Framework path not provided" unless @ARGV >= 1; + +my $framework_path = shift @ARGV; + +# Optionally accept NOWPLAYING_CLIENT path as second argument +my $maybe_helper_path = $ARGV[0] // ''; +if ($maybe_helper_path =~ m{NowPlayingTestClient} || $maybe_helper_path =~ m{/}) { + my $helper_path = shift @ARGV; + $ENV{NOWPLAYING_CLIENT} = $helper_path; +} + +my $framework_basename = File::Basename::basename($framework_path); +fail "Provided path is not a framework: $framework_path" + unless $framework_basename =~ s/\.framework$//; + +my $framework = File::Spec->catfile($framework_path, $framework_basename); +fail "Framework not found at $framework" unless -e $framework; + +my $handle = DynaLoader::dl_load_file($framework, 0) + or fail "Failed to load framework: $framework"; +my $function_name = shift @ARGV or fail "Missing function name"; +fail "Invalid function name: '$function_name'" + unless $function_name eq "stream" + || $function_name eq "get" + || $function_name eq "send" + || $function_name eq "seek" + || $function_name eq "shuffle" + || $function_name eq "repeat" + || $function_name eq "speed" + || $function_name eq "test"; + +sub parse_options { + my ($start_index) = @_; + my %arg_map; + my $i = $start_index; + while ($i <= $#ARGV) { + my $arg = $ARGV[$i]; + if ($arg =~ /^--([a-z\\-]+)(?:=(.*))?$/) { + my $key = $1; + my $value = defined $2 ? $2 : undef; + $arg_map{$key} = $value; + splice @ARGV, $i, 1; + } + elsif ($arg =~ /^-([a-zA-Z]+)$/) { + my @flags = split //, $1; + $arg_map{$_} = undef for @flags; + splice @ARGV, $i, 1; + } + else { + $i++; + } + } + return \%arg_map; +} + +sub env_func { + my $symbol_name = shift; + return "${symbol_name}_env"; +} + +sub set_env_param { + my ($func, $index, $name, $value) = @_; + $ENV{"MEDIAREMOTEADAPTER_PARAM_${func}_${index}_${name}"} = "$value"; +} + +sub set_env_option_unsafe { + my ($name, $value) = @_; + $name =~ s/-/_/g; + $ENV{"MEDIAREMOTEADAPTER_OPTION_${name}"} = defined $value ? "$value" : ""; +} + +sub set_env_option { + my ($options, $key) = @_; + my $value = $options->{$key}; + if (defined $value) { + fail "Unexpected value for option '$key'"; + } + set_env_option_unsafe($key, $value); +} + +sub set_env_option_value { + my ($options, $key) = @_; + my $value = $options->{$key}; + if (!defined $value) { + fail "Missing value for option '$key'"; + } + set_env_option_unsafe($key, $value); +} + +my $symbol_name = "adapter_$function_name"; +if ($function_name eq "send") { + my $id = shift @ARGV; + fail "Missing ID for '$function_name' command" unless defined $id; + set_env_param($symbol_name, 0, "command", "$id"); + $symbol_name = env_func($symbol_name); +} +elsif ($function_name eq "stream") { + my $options = parse_options(0); + foreach my $key (keys %{$options}) { + if ($key eq "no-diff") { + set_env_option($options, $key); + } + elsif ($key eq "debounce") { + set_env_option_value($options, $key); + } + elsif ($key eq "micros") { + set_env_option($options, $key); + } + elsif ($key eq "human-readable" || $key eq "h") { + set_env_option($options, "human-readable"); + } + else { + fail "Unrecognized option '$key'"; + } + } + $symbol_name = env_func($symbol_name); +} +elsif ($function_name eq "get") { + my $options = parse_options(0); + foreach my $key (keys %{$options}) { + if ($key eq "micros") { + set_env_option($options, $key); + } + elsif ($key eq "human-readable" || $key eq "h") { + set_env_option($options, "human-readable"); + } + elsif ($key eq "now") { + set_env_option($options, $key); + } + else { + fail "Unrecognized option '$key'"; + } + } + $symbol_name = env_func($symbol_name); +} +elsif ($function_name eq "seek") { + my $position = shift @ARGV; + fail "Missing position for '$function_name' command" unless defined $position; + set_env_param($symbol_name, 0, "position", "$position"); + $symbol_name = env_func($symbol_name); +} +elsif ($function_name eq "shuffle") { + my $mode = shift @ARGV; + fail "Missing mode for '$function_name' command" unless defined $mode; + set_env_param($symbol_name, 0, "mode", "$mode"); + $symbol_name = env_func($symbol_name); +} +elsif ($function_name eq "repeat") { + my $mode = shift @ARGV; + fail "Missing mode for '$function_name' command" unless defined $mode; + set_env_param($symbol_name, 0, "mode", "$mode"); + $symbol_name = env_func($symbol_name); +} +elsif ($function_name eq "speed") { + my $speed = shift @ARGV; + fail "Missing speed for '$function_name' command" unless defined $speed; + set_env_param($symbol_name, 0, "speed", "$speed"); + $symbol_name = env_func($symbol_name); +} +elsif ($function_name eq "test") { + $symbol_name = "_adapter_is_it_broken_yet"; +} + +if (defined shift @ARGV) { + fail "Too many arguments"; +} + +my $symbol = DynaLoader::dl_find_symbol($handle, "$symbol_name") + or fail "Symbol '$symbol_name' not found in $framework"; +DynaLoader::dl_install_xsub("main::$function_name", $symbol); + +eval { + no strict "refs"; + &{"main::$function_name"}(); +}; +if ($@) { + fail "Error executing $function_name: $@"; +} From 022fa187e3996d784dead34a586bf46af5325a67 Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Sun, 3 Aug 2025 22:10:22 +0530 Subject: [PATCH 2/3] Fix: Timeline Slider for NowPlaying --- .../NowPlayingController.swift | 8 +++-- DynamicIsland/managers/MusicManager.swift | 34 +++++++++++++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/DynamicIsland/MediaControllers/NowPlayingController.swift b/DynamicIsland/MediaControllers/NowPlayingController.swift index bea429a..69b754e 100644 --- a/DynamicIsland/MediaControllers/NowPlayingController.swift +++ b/DynamicIsland/MediaControllers/NowPlayingController.swift @@ -134,14 +134,16 @@ final class NowPlayingController: ObservableObject, MediaControllerProtocol { private func setupNowPlayingObserver() { let process = Process() guard - let scriptURL = Bundle.main.url(forResource: "mediaremote-adapter", withExtension: "pl"), - let frameworkPath = Bundle.main.privateFrameworksPath?.appending("/MediaRemoteAdapter.framework") + let scriptURL = Bundle.main.url(forResource: "mediaremote-adapter", withExtension: "pl") else { - print("⚠️ Could not find mediaremote-adapter.pl script or framework path - NowPlaying controller disabled") + print("⚠️ Could not find mediaremote-adapter.pl script - NowPlaying controller disabled") isResourcesAvailable = false return } + let bundlePath = Bundle.main.bundlePath + let frameworkPath = bundlePath + "/Contents/Frameworks/MediaRemoteAdapter.framework" + isResourcesAvailable = true process.executableURL = URL(fileURLWithPath: "/usr/bin/perl") process.arguments = [scriptURL.path, frameworkPath, "stream"] diff --git a/DynamicIsland/managers/MusicManager.swift b/DynamicIsland/managers/MusicManager.swift index b7e9314..cf141ca 100644 --- a/DynamicIsland/managers/MusicManager.swift +++ b/DynamicIsland/managers/MusicManager.swift @@ -265,6 +265,15 @@ class MusicManager: ObservableObject { self.bundleIdentifier = state.bundleIdentifier } + // Update shuffle and repeat state from controller + if state.isShuffled != self.isShuffled { + self.isShuffled = state.isShuffled + } + + if state.repeatMode != self.repeatMode { + self.repeatMode = state.repeatMode + } + self.timestampDate = state.lastUpdated } @@ -377,13 +386,38 @@ class MusicManager: ObservableObject { func toggleShuffle() { Task { + // Provide immediate UI feedback + await MainActor.run { + self.isShuffled.toggle() + } + + // Execute the actual command await activeController?.toggleShuffle() + + // Note: The actual state will be updated when the controller reports back + // via updateFromPlaybackState if the command provides status feedback } } func toggleRepeat() { Task { + // Provide immediate UI feedback + await MainActor.run { + switch self.repeatMode { + case .off: + self.repeatMode = .all + case .all: + self.repeatMode = .one + case .one: + self.repeatMode = .off + } + } + + // Execute the actual command await activeController?.toggleRepeat() + + // Note: The actual state will be updated when the controller reports back + // via updateFromPlaybackState if the command provides status feedback } } From 1b5925090a0bb57fa0e16cc24f4cc38d065065b2 Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Sun, 3 Aug 2025 22:26:18 +0530 Subject: [PATCH 3/3] Update SettingsView --- DynamicIsland.xcodeproj/project.pbxproj | 32 +- DynamicIsland/Localizable.xcstrings | 152 +- .../components/Settings/SettingsView.swift | 452 ++++- SettingsView.swift.md | 1635 +++++++++++++++++ 4 files changed, 2178 insertions(+), 93 deletions(-) create mode 100644 SettingsView.swift.md diff --git a/DynamicIsland.xcodeproj/project.pbxproj b/DynamicIsland.xcodeproj/project.pbxproj index 0c1810f..d754eed 100644 --- a/DynamicIsland.xcodeproj/project.pbxproj +++ b/DynamicIsland.xcodeproj/project.pbxproj @@ -22,10 +22,24 @@ C61F69420FB64DA98516CFBC /* MediaRemoteAdapter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = F4C1DD09EC134302938E84F6 /* MediaRemoteAdapter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; /* End PBXBuildFile section */ +/* Begin PBXCopyFilesBuildPhase section */ + B933CDB48C2F42C2887A9836 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 12; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + C61F69420FB64DA98516CFBC /* MediaRemoteAdapter.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ 7E81303D2E3FB40500E08F3F /* mediaremote-adapter.pl */ = {isa = PBXFileReference; lastKnownFileType = text.script.perl; path = "mediaremote-adapter.pl"; sourceTree = ""; }; 7E81303E2E3FB40500E08F3F /* NowPlayingTestClient */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; path = NowPlayingTestClient; sourceTree = ""; }; - F4C1DD09EC134302938E84F6 /* MediaRemoteAdapter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MediaRemoteAdapter.framework; path = "Frameworks/MediaRemoteAdapter.framework"; sourceTree = SOURCE_ROOT; }; + F4C1DD09EC134302938E84F6 /* MediaRemoteAdapter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MediaRemoteAdapter.framework; path = Frameworks/MediaRemoteAdapter.framework; sourceTree = SOURCE_ROOT; }; F54406E72CD4987A00C7AFD9 /* DynamicIsland.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DynamicIsland.app; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -57,20 +71,6 @@ }; /* End PBXFrameworksBuildPhase section */ -/* Begin PBXCopyFilesBuildPhase section */ - B933CDB48C2F42C2887A9836 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 12; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - C61F69420FB64DA98516CFBC /* MediaRemoteAdapter.framework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - /* Begin PBXGroup section */ 7E81303F2E3FB40500E08F3F /* mediaremote-adapter */ = { isa = PBXGroup; @@ -348,6 +348,7 @@ ); GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSAppleEventsUsageDescription = "Need for Spotify and Apple Music"; + INFOPLIST_KEY_NSCalendarsUsageDescription = "Need for Calendar Mode"; INFOPLIST_KEY_NSCameraUsageDescription = "We need your camera for Mirror mode"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; LD_RUNPATH_SEARCH_PATHS = ( @@ -381,6 +382,7 @@ ); GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSAppleEventsUsageDescription = "Need for Spotify and Apple Music"; + INFOPLIST_KEY_NSCalendarsUsageDescription = "Need for Calendar Mode"; INFOPLIST_KEY_NSCameraUsageDescription = "We need your camera for Mirror mode"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings index aa367ab..a7c7d42 100644 --- a/DynamicIsland/Localizable.xcstrings +++ b/DynamicIsland/Localizable.xcstrings @@ -34,9 +34,6 @@ }, "%lld" : { - }, - "%lld items" : { - }, "%lld%%" : { @@ -52,6 +49,18 @@ }, "• New feature 1" : { + }, + "3 items" : { + + }, + "5 items" : { + + }, + "7 items" : { + + }, + "10 items" : { + }, "About" : { "localizations" : { @@ -76,6 +85,9 @@ }, "Access Denied" : { + }, + "Actions" : { + }, "Activate" : { @@ -241,11 +253,14 @@ }, "Check for Updates…" : { + }, + "Choose a custom sound file that will play when the timer completes. Supported formats: MP3, M4A, WAV, AIFF." : { + }, "Choose a Music Source" : { }, - "Choose which performance graphs to display. 1-3 graphs use horizontal layout, 4+ graphs use compressed grid layout." : { + "Choose File" : { }, "Circle" : { @@ -253,15 +268,18 @@ }, "Clear" : { + }, + "Clear Clipboard History" : { + + }, + "Clear Data" : { + }, "Clipboard" : { }, "Clipboard History" : { - }, - "Clipboard history will be tracked and accessible via the header button." : { - }, "Clipboard Manager" : { @@ -278,15 +296,27 @@ } } } - }, - "Configure how many recent clipboard items to remember (minimum 3)." : { - }, "Continue" : { + }, + "Controls" : { + }, "Copy something to get started" : { + }, + "CPU Usage" : { + + }, + "Current custom timer: %@" : { + + }, + "Current History" : { + + }, + "Current Items" : { + }, "Custom" : { @@ -310,15 +340,27 @@ }, "Custom notch size - %.0f" : { + }, + "Custom Timer" : { + + }, + "Custom Timer Duration" : { + }, "Custom vizualizers (Lottie)" : { + }, + "Custom: %@" : { + }, "Customize in Settings" : { }, "Default" : { + }, + "Default: dynamic.m4a" : { + }, "Description" : { @@ -328,6 +370,12 @@ }, "Disabled" : { + }, + "Disk Read" : { + + }, + "Disk Write" : { + }, "Download" : { @@ -373,6 +421,9 @@ }, "dynamic.island" : { + }, + "Each graph can be individually enabled or disabled. Network activity shows download/upload speeds, and disk I/O shows read/write speeds." : { + }, "Easily copy, store, and manage your most-used content. Upgrade now for advanced features like multi-item storage and quick access!" : { @@ -413,6 +464,9 @@ }, "Enable music live activity" : { + }, + "Enable or disable the timer functionality in the Dynamic Island. The live activity toggle controls whether timer progress is shown in the expanded view." : { + }, "Enable sneak peek" : { @@ -423,13 +477,7 @@ "Enable timer feature in Settings to use this tab" : { }, - "Enable to access timer functionality in the Dynamic Island." : { - - }, - "Enable to monitor CPU, Memory, GPU, Network and Disk usage." : { - - }, - "Enable to track clipboard history and access recent items." : { + "Enable timer live activity" : { }, "Enhance your experience with HUDs" : { @@ -491,6 +539,9 @@ }, "Got it!" : { + }, + "GPU Usage" : { + }, "Gradient" : { @@ -510,7 +561,7 @@ "High" : { }, - "History size" : { + "History Size" : { }, "https://github.com/th-ch/youtube-music" : { @@ -541,6 +592,9 @@ }, "Installed extensions" : { + }, + "Last Updated" : { + }, "Later" : { @@ -550,6 +604,9 @@ }, "Live" : { + }, + "Live Performance Data" : { + }, "Lottie JSON URL" : { @@ -620,6 +677,9 @@ }, "Medium" : { + }, + "Memory Usage" : { + }, "Menubar icon" : { "extractionState" : "stale", @@ -640,12 +700,21 @@ }, "Minimum hover duration" : { + }, + "Minutes" : { + }, "Mirror" : { }, "Mirror shape" : { + }, + "Monitor clipboard changes and keep a history of recent copies. Use Cmd+Shift+V to quickly access clipboard history." : { + + }, + "Monitoring Status" : { + }, "More" : { @@ -661,6 +730,12 @@ }, "Need Restart" : { + }, + "Network Download" : { + + }, + "Network Upload" : { + }, "Never hide" : { @@ -812,6 +887,9 @@ }, "Remember last tab" : { + }, + "Reset to Default" : { + }, "Restart" : { @@ -835,6 +913,9 @@ } } } + }, + "Seconds" : { + }, "Select Calendars" : { @@ -847,6 +928,9 @@ }, "Selected animation" : { + }, + "Set a custom duration for the timer. This will be used when you press the 'Custom' button in the timer interface." : { + }, "Settings" : { "localizations" : { @@ -944,6 +1028,15 @@ }, "Start" : { + }, + "Start Demo Timer:" : { + + }, + "Start Monitoring" : { + + }, + "Starts a 5-minute demo timer to test the timer live activity feature." : { + }, "Stats" : { @@ -951,10 +1044,10 @@ "Stats Disabled" : { }, - "Stats tab will appear in the notch when enabled." : { + "Stop" : { }, - "Stop" : { + "Stop Monitoring" : { }, "Stopped" : { @@ -973,11 +1066,14 @@ "System features" : { }, - "System Monitoring" : { + "The clipboard icon appears in the header next to the settings gear when enabled. Use the global shortcut Cmd+Shift+V or click the icon to access history." : { }, "TheDynamicIsland" : { + }, + "This will permanently delete all stored clipboard history. The clipboard button is located to the left of the settings gear in the header." : { + }, "Time to Full Charge: %lld min" : { @@ -988,7 +1084,10 @@ "Timer Disabled" : { }, - "Timer tab will appear in the notch when enabled." : { + "Timer Feature" : { + + }, + "Timer Sound" : { }, "Toggle Notch Open:" : { @@ -1011,6 +1110,9 @@ }, "unmuted" : { + }, + "Update" : { + }, "Upgrade to Pro" : { @@ -1046,6 +1148,12 @@ }, "What's New" : { + }, + "When enabled, the Stats tab will display real-time system performance graphs. This feature requires system permissions and may use additional battery." : { + + }, + "With %lld graphs enabled, the Dynamic Island will expand horizontally to accommodate all graphs in a single row." : { + }, "You can now enjoy the app. If you want to tweak things further, you can always visit the settings." : { diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index a3a60a9..7d0bde2 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -14,6 +14,7 @@ import LottieUI import Sparkle import SwiftUI import SwiftUIIntrospect +import UniformTypeIdentifiers struct SettingsView: View { @StateObject var extensionManager = DynamicIslandExtensionManager() @@ -1126,6 +1127,16 @@ struct Shortcuts: View { Section { KeyboardShortcuts.Recorder("Toggle Notch Open:", name: .toggleNotchOpen) } + Section { + KeyboardShortcuts.Recorder("Start Demo Timer:", name: .startDemoTimer) + } header: { + Text("Timer") + } footer: { + Text("Starts a 5-minute demo timer to test the timer live activity feature.") + .multilineTextAlignment(.trailing) + .foregroundStyle(.secondary) + .font(.caption) + } } .navigationTitle("Shortcuts") } @@ -1178,62 +1189,318 @@ func warningBadge(_ text: String, _ description: String) -> some View { } struct TimerSettings: View { + @ObservedObject private var coordinator = DynamicIslandViewCoordinator.shared + @Default(.enableTimerFeature) var enableTimerFeature + @AppStorage("customTimerDuration") private var customTimerDuration: Double = 600 // 10 minutes default + @State private var customMinutes: Int = 10 + @State private var customSeconds: Int = 0 + var body: some View { Form { Section { Defaults.Toggle("Enable timer feature", key: .enableTimerFeature) - .onChange(of: Defaults[.enableTimerFeature]) { _, newValue in - if !newValue { - // Stop any running timers when feature is disabled - TimerManager.shared.stopTimer() + + if enableTimerFeature { + Toggle( + "Enable timer live activity", + isOn: $coordinator.timerLiveActivityEnabled + ) + .animation(.easeInOut, value: coordinator.timerLiveActivityEnabled) + } + } header: { + Text("Timer Feature") + } footer: { + Text("Enable or disable the timer functionality in the Dynamic Island. The live activity toggle controls whether timer progress is shown in the expanded view.") + } + + if enableTimerFeature { + Section { + VStack(alignment: .leading, spacing: 16) { + Text("Custom Timer Duration") + .font(.headline) + + HStack(spacing: 16) { + VStack { + Text("Minutes") + .font(.system(size: 14, weight: .medium)) + .foregroundStyle(.secondary) + + Picker(selection: $customMinutes, label: Text("Minutes")) { + ForEach(0...59, id: \.self) { minute in + Text("\(minute)").tag(minute) + } + } + .pickerStyle(.menu) + .frame(width: 80, height: 40) + } + + VStack { + Text("Seconds") + .font(.system(size: 14, weight: .medium)) + .foregroundStyle(.secondary) + + Picker(selection: $customSeconds, label: Text("Seconds")) { + ForEach(0...59, id: \.self) { second in + Text("\(second)").tag(second) + } + } + .pickerStyle(.menu) + .frame(width: 80, height: 40) + } + } + + HStack { + Text("Current custom timer: \(customTimerDisplayText)") + .foregroundStyle(.secondary) + Spacer() + Button("Update") { + customTimerDuration = Double(customMinutes * 60 + customSeconds) + } + .disabled(customMinutes == 0 && customSeconds == 0) } } - } header: { - Text("Timer") - } footer: { - if Defaults[.enableTimerFeature] { - Text("Timer tab will appear in the notch when enabled.") - } else { - Text("Enable to access timer functionality in the Dynamic Island.") + } header: { + Text("Custom Timer") + } footer: { + Text("Set a custom duration for the timer. This will be used when you press the 'Custom' button in the timer interface.") + } + + Section { + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("Timer Sound") + .font(.system(size: 16, weight: .medium)) + Spacer() + Button("Choose File") { + selectCustomTimerSound() + } + .buttonStyle(.bordered) + } + + if let customTimerSoundPath = UserDefaults.standard.string(forKey: "customTimerSoundPath") { + Text("Custom: \(URL(fileURLWithPath: customTimerSoundPath).lastPathComponent)") + .font(.caption) + .foregroundColor(.secondary) + } else { + Text("Default: dynamic.m4a") + .font(.caption) + .foregroundColor(.secondary) + } + + Button("Reset to Default") { + UserDefaults.standard.removeObject(forKey: "customTimerSoundPath") + } + .buttonStyle(.bordered) + .disabled(UserDefaults.standard.string(forKey: "customTimerSoundPath") == nil) + } + } header: { + Text("Timer Sound") + } footer: { + Text("Choose a custom sound file that will play when the timer completes. Supported formats: MP3, M4A, WAV, AIFF.") } } } .navigationTitle("Timer") + .onAppear { + let totalMinutes = Int(customTimerDuration) / 60 + customMinutes = totalMinutes + customSeconds = Int(customTimerDuration) % 60 + } + } + + private var customTimerDisplayText: String { + let totalMinutes = Int(customTimerDuration) / 60 + let seconds = Int(customTimerDuration) % 60 + let hours = totalMinutes / 60 + let minutes = totalMinutes % 60 + + if hours > 0 { + return "\(hours):\(String(format: "%02d", minutes)):\(String(format: "%02d", seconds))" + } else if minutes > 0 { + return "\(minutes):\(String(format: "%02d", seconds))" + } else { + return "\(seconds)s" + } + } + + private func selectCustomTimerSound() { + let panel = NSOpenPanel() + panel.title = "Select Timer Sound" + panel.allowedContentTypes = [.audio] + panel.allowsMultipleSelection = false + panel.canChooseDirectories = false + panel.canChooseFiles = true + + if panel.runModal() == .OK { + if let url = panel.url { + UserDefaults.standard.set(url.path, forKey: "customTimerSoundPath") + } + } } } struct StatsSettings: View { + @ObservedObject var statsManager = StatsManager.shared + @Default(.enableStatsFeature) var enableStatsFeature + @Default(.showCpuGraph) var showCpuGraph + @Default(.showMemoryGraph) var showMemoryGraph + @Default(.showGpuGraph) var showGpuGraph + @Default(.showNetworkGraph) var showNetworkGraph + @Default(.showDiskGraph) var showDiskGraph + + var enabledGraphsCount: Int { + [showCpuGraph, showMemoryGraph, showGpuGraph, showNetworkGraph, showDiskGraph].filter { $0 }.count + } + var body: some View { Form { Section { - Defaults.Toggle("Enable stats feature", key: .enableStatsFeature) - .onChange(of: Defaults[.enableStatsFeature]) { _, newValue in - if !newValue { - // Stop monitoring when feature is disabled - StatsManager.shared.stopMonitoring() + Defaults.Toggle("Enable system stats monitoring", key: .enableStatsFeature) + .onChange(of: enableStatsFeature) { _, newValue in + if newValue { + statsManager.startMonitoring() + } else { + statsManager.stopMonitoring() } } } header: { - Text("System Monitoring") + Text("General") } footer: { - if Defaults[.enableStatsFeature] { - Text("Stats tab will appear in the notch when enabled.") - } else { - Text("Enable to monitor CPU, Memory, GPU, Network and Disk usage.") - } + Text("When enabled, the Stats tab will display real-time system performance graphs. This feature requires system permissions and may use additional battery.") + .multilineTextAlignment(.trailing) + .foregroundStyle(.secondary) + .font(.caption) } - if Defaults[.enableStatsFeature] { + if enableStatsFeature { Section { - Defaults.Toggle("Show CPU graph", key: .showCpuGraph) - Defaults.Toggle("Show Memory graph", key: .showMemoryGraph) - Defaults.Toggle("Show GPU graph", key: .showGpuGraph) - Defaults.Toggle("Show Network graph", key: .showNetworkGraph) - Defaults.Toggle("Show Disk graph", key: .showDiskGraph) + Defaults.Toggle("CPU Usage", key: .showCpuGraph) + Defaults.Toggle("Memory Usage", key: .showMemoryGraph) + Defaults.Toggle("GPU Usage", key: .showGpuGraph) + Defaults.Toggle("Network Activity", key: .showNetworkGraph) + Defaults.Toggle("Disk I/O", key: .showDiskGraph) } header: { Text("Graph Visibility") } footer: { - Text("Choose which performance graphs to display. 1-3 graphs use horizontal layout, 4+ graphs use compressed grid layout.") + if enabledGraphsCount >= 4 { + Text("With \(enabledGraphsCount) graphs enabled, the Dynamic Island will expand horizontally to accommodate all graphs in a single row.") + .multilineTextAlignment(.trailing) + .foregroundStyle(.secondary) + .font(.caption) + } else { + Text("Each graph can be individually enabled or disabled. Network activity shows download/upload speeds, and disk I/O shows read/write speeds.") + .multilineTextAlignment(.trailing) + .foregroundStyle(.secondary) + .font(.caption) + } + } + + Section { + HStack { + Text("Monitoring Status") + Spacer() + HStack(spacing: 4) { + Circle() + .fill(statsManager.isMonitoring ? .green : .red) + .frame(width: 8, height: 8) + Text(statsManager.isMonitoring ? "Active" : "Stopped") + .foregroundStyle(.secondary) + } + } + + if statsManager.isMonitoring { + if showCpuGraph { + HStack { + Text("CPU Usage") + Spacer() + Text(statsManager.cpuUsageString) + .foregroundStyle(.secondary) + } + } + + if showMemoryGraph { + HStack { + Text("Memory Usage") + Spacer() + Text(statsManager.memoryUsageString) + .foregroundStyle(.secondary) + } + } + + if showGpuGraph { + HStack { + Text("GPU Usage") + Spacer() + Text(statsManager.gpuUsageString) + .foregroundStyle(.secondary) + } + } + + if showNetworkGraph { + HStack { + Text("Network Download") + Spacer() + Text(String(format: "%.1f MB/s", statsManager.networkDownload)) + .foregroundStyle(.secondary) + } + + HStack { + Text("Network Upload") + Spacer() + Text(String(format: "%.1f MB/s", statsManager.networkUpload)) + .foregroundStyle(.secondary) + } + } + + if showDiskGraph { + HStack { + Text("Disk Read") + Spacer() + Text(String(format: "%.1f MB/s", statsManager.diskRead)) + .foregroundStyle(.secondary) + } + + HStack { + Text("Disk Write") + Spacer() + Text(String(format: "%.1f MB/s", statsManager.diskWrite)) + .foregroundStyle(.secondary) + } + } + + HStack { + Text("Last Updated") + Spacer() + Text(statsManager.lastUpdated, style: .relative) + .foregroundStyle(.secondary) + } + } + } header: { + Text("Live Performance Data") + } + + Section { + HStack { + Button(statsManager.isMonitoring ? "Stop Monitoring" : "Start Monitoring") { + if statsManager.isMonitoring { + statsManager.stopMonitoring() + } else { + statsManager.startMonitoring() + } + } + .buttonStyle(.borderedProminent) + .foregroundColor(statsManager.isMonitoring ? .red : .blue) + + Spacer() + + Button("Clear Data") { + statsManager.clearHistory() + } + .buttonStyle(.bordered) + .disabled(statsManager.isMonitoring) + } + } header: { + Text("Controls") } } } @@ -1242,55 +1509,128 @@ struct StatsSettings: View { } struct ClipboardSettings: View { + @ObservedObject var clipboardManager = ClipboardManager.shared + @Default(.enableClipboardManager) var enableClipboardManager + @Default(.clipboardHistorySize) var clipboardHistorySize + @Default(.showClipboardIcon) var showClipboardIcon + var body: some View { Form { Section { - Defaults.Toggle("Enable clipboard manager", key: .enableClipboardManager) - .onChange(of: Defaults[.enableClipboardManager]) { _, newValue in - if newValue { - ClipboardManager.shared.startMonitoring() + Defaults.Toggle("Enable Clipboard Manager", key: .enableClipboardManager) + .onChange(of: enableClipboardManager) { _, enabled in + if enabled { + clipboardManager.startMonitoring() } else { - ClipboardManager.shared.stopMonitoring() + clipboardManager.stopMonitoring() } } } header: { Text("Clipboard Manager") } footer: { - if Defaults[.enableClipboardManager] { - Text("Clipboard history will be tracked and accessible via the header button.") - } else { - Text("Enable to track clipboard history and access recent items.") - } + Text("Monitor clipboard changes and keep a history of recent copies. Use Cmd+Shift+V to quickly access clipboard history.") } - if Defaults[.enableClipboardManager] { + if enableClipboardManager { Section { - Defaults.Toggle("Show clipboard icon", key: .showClipboardIcon) + Defaults.Toggle("Show Clipboard Icon", key: .showClipboardIcon) - VStack(alignment: .leading, spacing: 8) { - HStack { - Text("History size") - Spacer() - Text("\(Defaults[.clipboardHistorySize]) items") - .foregroundStyle(.secondary) + HStack { + Text("History Size") + Spacer() + Picker("History Size", selection: $clipboardHistorySize) { + Text("3 items").tag(3) + Text("5 items").tag(5) + Text("7 items").tag(7) + Text("10 items").tag(10) } - Slider( - value: Binding( - get: { Double(Defaults[.clipboardHistorySize]) }, - set: { Defaults[.clipboardHistorySize] = Int($0) } - ), - in: 3...10, - step: 1 - ) + .pickerStyle(.menu) + .frame(width: 100) + } + + HStack { + Text("Current Items") + Spacer() + Text("\(clipboardManager.clipboardHistory.count)") + .foregroundColor(.secondary) + } + + HStack { + Text("Monitoring Status") + Spacer() + Text(clipboardManager.isMonitoring ? "Active" : "Stopped") + .foregroundColor(clipboardManager.isMonitoring ? .green : .secondary) } } header: { Text("Settings") } footer: { - Text("Configure how many recent clipboard items to remember (minimum 3).") + Text("The clipboard icon appears in the header next to the settings gear when enabled. Use the global shortcut Cmd+Shift+V or click the icon to access history.") + } + + Section { + Button("Clear Clipboard History") { + clipboardManager.clearHistory() + } + .foregroundColor(.red) + .disabled(clipboardManager.clipboardHistory.isEmpty) + } header: { + Text("Actions") + } footer: { + Text("This will permanently delete all stored clipboard history. The clipboard button is located to the left of the settings gear in the header.") + } + + if !clipboardManager.clipboardHistory.isEmpty { + Section { + ForEach(clipboardManager.clipboardHistory) { item in + VStack(alignment: .leading, spacing: 4) { + HStack { + Image(systemName: item.type.icon) + .foregroundColor(.blue) + .frame(width: 16) + Text(item.type.displayName) + .font(.caption) + .foregroundColor(.secondary) + Spacer() + Text(timeAgoString(from: item.timestamp)) + .font(.caption2) + .foregroundColor(.secondary) + } + Text(item.preview) + .font(.system(.body, design: .monospaced)) + .lineLimit(2) + } + .padding(.vertical, 2) + } + } header: { + Text("Current History") + } } } } + .formStyle(.grouped) .navigationTitle("Clipboard") + .onAppear { + if enableClipboardManager && !clipboardManager.isMonitoring { + clipboardManager.startMonitoring() + } + } + } + + private func timeAgoString(from date: Date) -> String { + let interval = Date().timeIntervalSince(date) + + if interval < 60 { + return "Just now" + } else if interval < 3600 { + let minutes = Int(interval / 60) + return "\(minutes)m ago" + } else if interval < 86400 { + let hours = Int(interval / 3600) + return "\(hours)h ago" + } else { + let days = Int(interval / 86400) + return "\(days)d ago" + } } } diff --git a/SettingsView.swift.md b/SettingsView.swift.md new file mode 100644 index 0000000..d8817e4 --- /dev/null +++ b/SettingsView.swift.md @@ -0,0 +1,1635 @@ +// +// SettingsView.swift +// DynamicIsland +// +// Created by Richard Kunkli on 07/08/2024. +// Modified by Hariharan Mudaliar + +import AVFoundation +import Defaults +import EventKit +import KeyboardShortcuts +import LaunchAtLogin +import LottieUI +import Sparkle +import SwiftUI +import SwiftUIIntrospect +import UniformTypeIdentifiers + +struct SettingsView: View { + @StateObject var extensionManager = DynamicIslandExtensionManager() + @StateObject private var calendarManager = CalendarManager() + + @State private var selectedTab = "General" + + let updaterController: SPUStandardUpdaterController? + + init(updaterController: SPUStandardUpdaterController? = nil) { + self.updaterController = updaterController + } + + var body: some View { + NavigationSplitView { + List(selection: $selectedTab) { + NavigationLink(value: "General") { + Label("General", systemImage: "gear") + } + NavigationLink(value: "Appearance") { + Label("Appearance", systemImage: "eye") + } + NavigationLink(value: "Media") { + Label("Media", systemImage: "play.laptopcomputer") + } + NavigationLink(value: "Timer") { + Label("Timer", systemImage: "timer") + } + NavigationLink(value: "Calendar") { + Label("Calendar", systemImage: "calendar") + } + if extensionManager.installedExtensions + .contains(where: { $0.bundleIdentifier == hudExtension }) { + NavigationLink(value: "HUD") { + Label("HUDs", systemImage: "dial.medium.fill") + } + } + NavigationLink(value: "Battery") { + Label("Battery", systemImage: "battery.100.bolt") + } + if extensionManager.installedExtensions + .contains(where: { $0.bundleIdentifier == downloadManagerExtension }) { + NavigationLink(value: "Downloads") { + Label("Downloads", systemImage: "square.and.arrow.down") + } + } + NavigationLink(value: "Shelf") { + Label("Shelf", systemImage: "books.vertical") + } + NavigationLink(value: "Stats") { + Label("Stats", systemImage: "chart.line.uptrend.xyaxis") + } + NavigationLink(value: "Clipboard") { + Label("Clipboard", systemImage: "doc.on.clipboard") + } + NavigationLink(value: "Shortcuts") { + Label("Shortcuts", systemImage: "keyboard") + } + NavigationLink(value: "Extensions") { + Label("Extensions", systemImage: "puzzlepiece.extension") + } + NavigationLink(value: "About") { + Label("About", systemImage: "info.circle") + } + } + .listStyle(SidebarListStyle()) + .toolbar(removing: .sidebarToggle) + .navigationSplitViewColumnWidth(200) + } detail: { + Group { + switch selectedTab { + case "General": + GeneralSettings() + case "Appearance": + Appearance() + case "Media": + Media() + case "Timer": + TimerSettings() + case "Calendar": + CalendarSettings() + case "HUD": + HUD() + case "Battery": + Charge() + case "Downloads": + Downloads() + case "Shelf": + Shelf() + case "Stats": + Stats() + case "Clipboard": + ClipboardSettings() + case "Shortcuts": + Shortcuts() + case "Extensions": + Extensions() + case "About": + if let controller = updaterController { + About(updaterController: controller) + } else { + // Fallback with a default controller + About(updaterController: SPUStandardUpdaterController(startingUpdater: false, updaterDelegate: nil, userDriverDelegate: nil)) + } + default: + GeneralSettings() + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + .navigationSplitViewStyle(.balanced) + .toolbar(removing: .sidebarToggle) + .toolbar { + Button("") {} // Empty label, does nothing + .controlSize(.extraLarge) + .opacity(0) // Invisible, but reserves space for a consistent look between tabs + .disabled(true) + } + .environmentObject(extensionManager) + .environmentObject(calendarManager) + .formStyle(.grouped) + .frame(width: 700) + .background(Color(NSColor.windowBackgroundColor)) + } +} + +struct GeneralSettings: View { + @State private var screens: [String] = NSScreen.screens.compactMap { $0.localizedName } + @EnvironmentObject var vm: DynamicIslandViewModel + @ObservedObject var coordinator = DynamicIslandViewCoordinator.shared + + @Default(.mirrorShape) var mirrorShape + @Default(.showEmojis) var showEmojis + @Default(.gestureSensitivity) var gestureSensitivity + @Default(.minimumHoverDuration) var minimumHoverDuration + @Default(.nonNotchHeight) var nonNotchHeight + @Default(.nonNotchHeightMode) var nonNotchHeightMode + @Default(.notchHeight) var notchHeight + @Default(.notchHeightMode) var notchHeightMode + @Default(.showOnAllDisplays) var showOnAllDisplays + @Default(.automaticallySwitchDisplay) var automaticallySwitchDisplay + @Default(.enableGestures) var enableGestures + @Default(.openNotchOnHover) var openNotchOnHover + @Default(.alwaysHideInFullscreen) var alwaysHideInFullscreen + + var body: some View { + Form { + Section { + Defaults.Toggle("Menubar icon", key: .menubarIcon) + LaunchAtLogin.Toggle("Launch at login") + Defaults.Toggle(key: .showOnAllDisplays) { + Text("Show on all displays") + } + .onChange(of: showOnAllDisplays) { + NotificationCenter.default.post(name: Notification.Name.showOnAllDisplaysChanged, object: nil) + } + Picker("Show on a specific display", selection: $coordinator.preferredScreen) { + ForEach(screens, id: \.self) { screen in + Text(screen) + } + } + .onChange(of: NSScreen.screens) { + screens = NSScreen.screens.compactMap({$0.localizedName}) + } + .disabled(showOnAllDisplays) + Defaults.Toggle("Automatically switch displays", key: .automaticallySwitchDisplay) + .onChange(of: automaticallySwitchDisplay) { + NotificationCenter.default.post(name: Notification.Name.automaticallySwitchDisplayChanged, object: nil) + } + .disabled(showOnAllDisplays) + } header: { + Text("System features") + } + + Section { + Picker(selection: $notchHeightMode, label: + Text("Notch display height")) { + Text("Match real notch size") + .tag(WindowHeightMode.matchRealNotchSize) + Text("Match menubar height") + .tag(WindowHeightMode.matchMenuBar) + Text("Custom height") + .tag(WindowHeightMode.custom) + } + .onChange(of: notchHeightMode) { + switch notchHeightMode { + case .matchRealNotchSize: + notchHeight = 38 + case .matchMenuBar: + notchHeight = 44 + case .custom: + notchHeight = 38 + } + NotificationCenter.default.post(name: Notification.Name.notchHeightChanged, object: nil) + } + if notchHeightMode == .custom { + Slider(value: $notchHeight, in: 15...45, step: 1) { + Text("Custom notch size - \(notchHeight, specifier: "%.0f")") + } + .onChange(of: notchHeight) { + NotificationCenter.default.post(name: Notification.Name.notchHeightChanged, object: nil) + } + } + Picker("Non-notch display height", selection: $nonNotchHeightMode) { + Text("Match menubar height") + .tag(WindowHeightMode.matchMenuBar) + Text("Match real notch size") + .tag(WindowHeightMode.matchRealNotchSize) + Text("Custom height") + .tag(WindowHeightMode.custom) + } + .onChange(of: nonNotchHeightMode) { + switch nonNotchHeightMode { + case .matchMenuBar: + nonNotchHeight = 24 + case .matchRealNotchSize: + nonNotchHeight = 32 + case .custom: + nonNotchHeight = 32 + } + NotificationCenter.default.post(name: Notification.Name.notchHeightChanged, object: nil) + } + if nonNotchHeightMode == .custom { + Slider(value: $nonNotchHeight, in: 0...40, step: 1) { + Text("Custom notch size - \(nonNotchHeight, specifier: "%.0f")") + } + .onChange(of: nonNotchHeight) { + NotificationCenter.default.post(name: Notification.Name.notchHeightChanged, object: nil) + } + } + } header: { + Text("Notch Height") + } + + NotchBehaviour() + + gestureControls() + } + .toolbar { + Button("Quit app") { + NSApp.terminate(self) + } + .controlSize(.extraLarge) + } + .navigationTitle("General") + .onChange(of: openNotchOnHover) { + if !openNotchOnHover { + enableGestures = true + } + } + } + + @ViewBuilder + func gestureControls() -> some View { + Section { + Defaults.Toggle("Enable gestures", key: .enableGestures) + .disabled(!openNotchOnHover) + if enableGestures { + Toggle("Media change with horizontal gestures", isOn: .constant(false)) + .disabled(true) + Defaults.Toggle("Close gesture", key: .closeGestureEnabled) + Slider(value: $gestureSensitivity, in: 100...300, step: 100) { + HStack { + Text("Gesture sensitivity") + Spacer() + Text(Defaults[.gestureSensitivity] == 100 ? "High" : Defaults[.gestureSensitivity] == 200 ? "Medium" : "Low") + .foregroundStyle(.secondary) + } + } + } + } header: { + HStack { + Text("Gesture control") + customBadge(text: "Beta") + } + } footer: { + Text("Two-finger swipe up on notch to close, two-finger swipe down on notch to open when **Open notch on hover** option is disabled") + .multilineTextAlignment(.trailing) + .foregroundStyle(.secondary) + .font(.caption) + } + } + + @ViewBuilder + func NotchBehaviour() -> some View { + Section { + Defaults.Toggle("Extend hover area", key: .extendHoverArea) + Defaults.Toggle("Enable haptics", key: .enableHaptics) + Defaults.Toggle("Open notch on hover", key: .openNotchOnHover) + Toggle("Remember last tab", isOn: $coordinator.openLastTabByDefault) + if openNotchOnHover { + Slider(value: $minimumHoverDuration, in: 0...1, step: 0.1) { + HStack { + Text("Minimum hover duration") + Spacer() + Text("\(minimumHoverDuration, specifier: "%.1f")s") + .foregroundStyle(.secondary) + } + } + .onChange(of: minimumHoverDuration) { + NotificationCenter.default.post(name: Notification.Name.notchHeightChanged, object: nil) + } + } + } header: { + Text("Notch behavior") + } + } +} + +struct Charge: View { + var body: some View { + Form { + Section { + Defaults.Toggle("Show battery indicator", key: .showBatteryIndicator) + Defaults.Toggle("Show power status notifications", key: .showPowerStatusNotifications) + } header: { + Text("General") + } + Section { + Defaults.Toggle("Show battery percentage", key: .showBatteryPercentage) + Defaults.Toggle("Show power status icons", key: .showPowerStatusIcons) + } header: { + Text("Battery Information") + } + } + .navigationTitle("Battery") + } +} + +struct Downloads: View { + @Default(.selectedDownloadIndicatorStyle) var selectedDownloadIndicatorStyle + @Default(.selectedDownloadIconStyle) var selectedDownloadIconStyle + var body: some View { + Form { + warningBadge("We don't support downloads yet", "It will be supported later on.") + Section { + Defaults.Toggle("Show download progress", key: .enableDownloadListener) + .disabled(true) + Defaults.Toggle("Enable Safari Downloads", key: .enableSafariDownloads) + .disabled(!Defaults[.enableDownloadListener]) + Picker("Download indicator style", selection: $selectedDownloadIndicatorStyle) { + Text("Progress bar") + .tag(DownloadIndicatorStyle.progress) + Text("Percentage") + .tag(DownloadIndicatorStyle.percentage) + } + Picker("Download icon style", selection: $selectedDownloadIconStyle) { + Text("Only app icon") + .tag(DownloadIconStyle.onlyAppIcon) + Text("Only download icon") + .tag(DownloadIconStyle.onlyIcon) + Text("Both") + .tag(DownloadIconStyle.iconAndAppIcon) + } + + } header: { + HStack { + Text("Download indicators") + comingSoonTag() + } + } + Section { + List { + ForEach([].indices, id: \.self) { index in + Text("\(index)") + } + } + .frame(minHeight: 96) + .overlay { + if true { + Text("No excluded apps") + .foregroundStyle(Color(.secondaryLabelColor)) + } + } + .actionBar(padding: 0) { + Group { + Button {} label: { + Image(systemName: "plus") + .frame(width: 25, height: 16, alignment: .center) + .contentShape(Rectangle()) + .foregroundStyle(.secondary) + } + + Divider() + Button {} label: { + Image(systemName: "minus") + .frame(width: 20, height: 16, alignment: .center) + .contentShape(Rectangle()) + .foregroundStyle(.secondary) + } + } + } + } header: { + HStack(spacing: 4) { + Text("Exclude apps") + comingSoonTag() + } + } + } + .navigationTitle("Downloads") + } +} + +struct HUD: View { + @EnvironmentObject var vm: DynamicIslandViewModel + @Default(.inlineHUD) var inlineHUD + @Default(.enableGradient) var enableGradient + @ObservedObject var coordinator = DynamicIslandViewCoordinator.shared + var body: some View { + Form { + Section { + Toggle("Enable HUD replacement", isOn: $coordinator.hudReplacement) + } header: { + Text("General") + } + Section { + Picker("HUD style", selection: $inlineHUD) { + Text("Default") + .tag(false) + Text("Inline") + .tag(true) + } + .onChange(of: Defaults[.inlineHUD]) { + if Defaults[.inlineHUD] { + withAnimation { + Defaults[.systemEventIndicatorShadow] = false + Defaults[.enableGradient] = false + } + } + } + Picker("Progressbar style", selection: $enableGradient) { + Text("Hierarchical") + .tag(false) + Text("Gradient") + .tag(true) + } + Defaults.Toggle("Enable glowing effect", key: .systemEventIndicatorShadow) + Defaults.Toggle("Use accent color", key: .systemEventIndicatorUseAccent) + } header: { + HStack { + Text("Appearance") + } + } + } + .navigationTitle("HUDs") + } +} + +struct Media: View { + @Default(.waitInterval) var waitInterval + @Default(.mediaController) var mediaController + @ObservedObject var coordinator = DynamicIslandViewCoordinator.shared + @Default(.hideNotchOption) var hideNotchOption + @Default(.enableSneakPeek) private var enableSneakPeek + @Default(.sneakPeekStyles) var sneakPeekStyles + + var body: some View { + Form { + Section { + Picker("Music Source", selection: $mediaController) { + ForEach(availableMediaControllers) { controller in + Text(controller.rawValue).tag(controller) + } + } + .onChange(of: mediaController) { _, _ in + NotificationCenter.default.post( + name: Notification.Name.mediaControllerChanged, + object: nil + ) + } + } header: { + Text("Media Source") + } footer: { + if MusicManager.shared.isNowPlayingDeprecated { + HStack { + Text("YouTube Music requires this third-party app to be installed: ") + .foregroundStyle(.secondary) + .font(.caption) + Link("https://github.com/th-ch/youtube-music", destination: URL(string: "https://github.com/th-ch/youtube-music")!) + .font(.caption) + .foregroundColor(.blue) // Ensures it's visibly a link + } + } else { + Text("'Now Playing' was the only option on previous versions and works with all media apps.") + .foregroundStyle(.secondary) + .font(.caption) + } + } + Section { + Toggle( + "Enable music live activity", + isOn: $coordinator.musicLiveActivityEnabled + ) + .animation(.easeInOut, value: coordinator.musicLiveActivityEnabled) + + Toggle("Enable sneak peek", isOn: $enableSneakPeek) + Picker("Sneak Peek Style", selection: $sneakPeekStyles){ + ForEach(SneakPeekStyle.allCases) { style in + Text(style.rawValue).tag(style) + } + }.disabled(!enableSneakPeek) + HStack { + Stepper(value: $waitInterval, in: 0...10, step: 1) { + HStack { + Text("Media inactivity timeout") + Spacer() + Text("\(Defaults[.waitInterval], specifier: "%.0f") seconds") + .foregroundStyle(.secondary) + } + } + } + } header: { + Text("Media playback live activity") + } + + Picker(selection: $hideNotchOption, label: + HStack { + Text("Hide DynamicIsland Options") + customBadge(text: "Beta") + }) { + Text("Always hide in fullscreen").tag(HideNotchOption.always) + Text("Hide only when NowPlaying app is in fullscreen").tag(HideNotchOption.nowPlayingOnly) + Text("Never hide").tag(HideNotchOption.never) + } + .onChange(of: hideNotchOption) { _, newValue in + Defaults[.alwaysHideInFullscreen] = newValue == .always + Defaults[.enableFullscreenMediaDetection] = newValue != .never + } + } + .navigationTitle("Media") + } + + // Only show controller options that are available on this macOS version + private var availableMediaControllers: [MediaControllerType] { + if MusicManager.shared.isNowPlayingDeprecated { + return MediaControllerType.allCases.filter { $0 != .nowPlaying } + } else { + return MediaControllerType.allCases + } + } +} + +struct CalendarSettings: View { + @ObservedObject private var calendarManager = CalendarManager() + @Default(.showCalendar) var showCalendar: Bool + + var body: some View { + Form { + if calendarManager.authorizationStatus != .fullAccess { + Text("Calendar access is denied. Please enable it in System Settings.") + .foregroundColor(.red) + .multilineTextAlignment(.center) + .padding() + Button("Open System Settings") { + if let settingsURL = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Calendars") { + NSWorkspace.shared.open(settingsURL) + } + } + } else { + Toggle("Show calendar", isOn: $showCalendar) + Section(header: Text("Select Calendars")) { + List { + ForEach(calendarManager.allCalendars, id: \.id) { calendar in + Toggle(isOn: Binding( + get: { calendarManager.getCalendarSelected(calendar) }, + set: { isSelected in + Task { + await calendarManager.setCalendarSelected(calendar, isSelected: isSelected) + } + } + )) { + Text(calendar.title) + } + } + } + } + } + } + .onAppear { + Task { + await calendarManager.checkCalendarAuthorization() + } + } + // Add navigation title if it's missing or adjust as needed + .navigationTitle("Calendar") + } +} + +struct TimerSettings: View { + @ObservedObject private var coordinator = DynamicIslandViewCoordinator.shared + @Default(.enableTimerFeature) var enableTimerFeature + @AppStorage("customTimerDuration") private var customTimerDuration: Double = 600 // 10 minutes default + @State private var customMinutes: Int = 10 + @State private var customSeconds: Int = 0 + + var body: some View { + Form { + Section { + Defaults.Toggle("Enable timer feature", key: .enableTimerFeature) + + if enableTimerFeature { + Toggle( + "Enable timer live activity", + isOn: $coordinator.timerLiveActivityEnabled + ) + .animation(.easeInOut, value: coordinator.timerLiveActivityEnabled) + } + } header: { + Text("Timer Feature") + } footer: { + Text("Enable or disable the timer functionality in the Dynamic Island. The live activity toggle controls whether timer progress is shown in the expanded view.") + } + + if enableTimerFeature { + Section { + VStack(alignment: .leading, spacing: 16) { + Text("Custom Timer Duration") + .font(.headline) + + HStack(spacing: 16) { + VStack { + Text("Minutes") + .font(.system(size: 14, weight: .medium)) + .foregroundStyle(.secondary) + + Picker(selection: $customMinutes, label: Text("Minutes")) { + ForEach(0...59, id: \.self) { minute in + Text("\(minute)").tag(minute) + } + } + .pickerStyle(.menu) + .frame(width: 80, height: 40) + } + + VStack { + Text("Seconds") + .font(.system(size: 14, weight: .medium)) + .foregroundStyle(.secondary) + + Picker(selection: $customSeconds, label: Text("Seconds")) { + ForEach(0...59, id: \.self) { second in + Text("\(second)").tag(second) + } + } + .pickerStyle(.menu) + .frame(width: 80, height: 40) + } + } + + HStack { + Text("Current custom timer: \(customTimerDisplayText)") + .foregroundStyle(.secondary) + Spacer() + Button("Update") { + customTimerDuration = Double(customMinutes * 60 + customSeconds) + } + .disabled(customMinutes == 0 && customSeconds == 0) + } + } + } header: { + Text("Custom Timer") + } footer: { + Text("Set a custom duration for the timer. This will be used when you press the 'Custom' button in the timer interface.") + } + + Section { + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("Timer Sound") + .font(.system(size: 16, weight: .medium)) + Spacer() + Button("Choose File") { + selectCustomTimerSound() + } + .buttonStyle(.bordered) + } + + if let customTimerSoundPath = UserDefaults.standard.string(forKey: "customTimerSoundPath") { + Text("Custom: \(URL(fileURLWithPath: customTimerSoundPath).lastPathComponent)") + .font(.caption) + .foregroundColor(.secondary) + } else { + Text("Default: dynamic.m4a") + .font(.caption) + .foregroundColor(.secondary) + } + + Button("Reset to Default") { + UserDefaults.standard.removeObject(forKey: "customTimerSoundPath") + } + .buttonStyle(.bordered) + .disabled(UserDefaults.standard.string(forKey: "customTimerSoundPath") == nil) + } + } header: { + Text("Timer Sound") + } footer: { + Text("Choose a custom sound file that will play when the timer completes. Supported formats: MP3, M4A, WAV, AIFF.") + } + } + } + .navigationTitle("Timer") + .onAppear { + let totalMinutes = Int(customTimerDuration) / 60 + customMinutes = totalMinutes + customSeconds = Int(customTimerDuration) % 60 + } + } + + private var customTimerDisplayText: String { + let totalMinutes = Int(customTimerDuration) / 60 + let seconds = Int(customTimerDuration) % 60 + let hours = totalMinutes / 60 + let minutes = totalMinutes % 60 + + if hours > 0 { + return "\(hours):\(String(format: "%02d", minutes)):\(String(format: "%02d", seconds))" + } else if minutes > 0 { + return "\(minutes):\(String(format: "%02d", seconds))" + } else { + return "\(seconds)s" + } + } + + private func selectCustomTimerSound() { + let panel = NSOpenPanel() + panel.title = "Select Timer Sound" + panel.allowedContentTypes = [.audio] + panel.allowsMultipleSelection = false + panel.canChooseDirectories = false + panel.canChooseFiles = true + + if panel.runModal() == .OK { + if let url = panel.url { + UserDefaults.standard.set(url.path, forKey: "customTimerSoundPath") + } + } + } +} + +struct About: View { + @State private var showBuildNumber: Bool = false + let updaterController: SPUStandardUpdaterController + @Environment(\.openWindow) var openWindow + var body: some View { + VStack { + Form { + Section { + HStack { + Text("Release name") + Spacer() + Text(Defaults[.releaseName]) + .foregroundStyle(.secondary) + } + HStack { + Text("Version") + Spacer() + if showBuildNumber { + Text("(\(Bundle.main.buildVersionNumber ?? ""))") + .foregroundStyle(.secondary) + } + Text(Bundle.main.releaseVersionNumber ?? "unkown") + .foregroundStyle(.secondary) + } + .onTapGesture { + withAnimation { + showBuildNumber.toggle() + } + } + } header: { + Text("Version info") + } + + UpdaterSettingsView(updater: updaterController.updater) + + HStack(spacing: 30) { + Spacer(minLength: 0) + Button { + NSWorkspace.shared.open(sponsorPage) + } label: { + VStack(spacing: 5) { + Image(systemName: "cup.and.saucer.fill") + .imageScale(.large) + Text("Support Us") + .foregroundStyle(.white) + } + .contentShape(Rectangle()) + } + Spacer(minLength: 0) + Button { + NSWorkspace.shared.open(productPage) + } label: { + VStack(spacing: 5) { + Image("Github") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 18) + Text("GitHub") + .foregroundStyle(.white) + } + .contentShape(Rectangle()) + } + Spacer(minLength: 0) + } + .buttonStyle(PlainButtonStyle()) + } + VStack(spacing: 0) { + Divider() + Text("Made with 🫶🏻 by not so boring not.people") + .foregroundStyle(.secondary) + .padding(.top, 5) + .padding(.bottom, 7) + .multilineTextAlignment(.center) + .padding(.horizontal, 10) + } + .frame(maxWidth: .infinity, alignment: .center) + } + .toolbar { +// Button("Welcome window") { +// openWindow(id: "onboarding") +// } +// .controlSize(.extraLarge) + CheckForUpdatesView(updater: updaterController.updater) + } + .navigationTitle("About") + } +} + +struct Shelf: View { + var body: some View { + Form { + Section { + Defaults.Toggle("Enable shelf", key: .dynamicShelf) + Defaults.Toggle("Open shelf tab by default if items added", key: .openShelfByDefault) + } header: { + HStack { + Text("General") + } + } + } + .navigationTitle("Shelf") + } +} + +struct Extensions: View { + @EnvironmentObject var extensionManager: DynamicIslandExtensionManager + @State private var effectTrigger: Bool = false + var body: some View { + Form { + //warningBadge("We don't support extensions yet") // Uhhhh You do? <><><> Oori.S + Section { + List { + ForEach(extensionManager.installedExtensions.indices, id: \.self) { index in + let item = extensionManager.installedExtensions[index] + HStack { + AppIcon(for: item.bundleIdentifier) + .resizable() + .frame(width: 24, height: 24) + Text(item.name) + ListItemPopover { + Text("Description") + } + Spacer(minLength: 0) + HStack(spacing: 6) { + Circle() + .frame(width: 6, height: 6) + .foregroundColor(isExtensionRunning(item.bundleIdentifier) ? .green : item.status == .disabled ? .gray : .red) + .conditionalModifier(isExtensionRunning(item.bundleIdentifier)) { view in + view + .shadow(color: .green, radius: 3) + } + Text(isExtensionRunning(item.bundleIdentifier) ? "Running" : item.status == .disabled ? "Disabled" : "Stopped") + .contentTransition(.numericText()) + .foregroundStyle(.secondary) + .font(.footnote) + } + .frame(width: 60, alignment: .leading) + + Menu(content: { + Button("Restart") { + let ws = NSWorkspace.shared + + if let ext = ws.runningApplications.first(where: { $0.bundleIdentifier == item.bundleIdentifier }) { + ext.terminate() + } + + if let appURL = ws.urlForApplication(withBundleIdentifier: item.bundleIdentifier) { + ws.openApplication(at: appURL, configuration: .init(), completionHandler: nil) + } + } + .keyboardShortcut("R", modifiers: .command) + Button("Disable") { + if let ext = NSWorkspace.shared.runningApplications.first(where: { $0.bundleIdentifier == item.bundleIdentifier }) { + ext.terminate() + } + extensionManager.installedExtensions[index].status = .disabled + } + .keyboardShortcut("D", modifiers: .command) + Divider() + Button("Uninstall", role: .destructive) { + // + } + }, label: { + Image(systemName: "ellipsis.circle") + .foregroundStyle(.secondary) + }) + .controlSize(.regular) + } + .buttonStyle(PlainButtonStyle()) + .padding(.vertical, 5) + } + } + .frame(minHeight: 120) + .actionBar { + Button {} label: { + HStack(spacing: 3) { + Image(systemName: "plus") + Text("Add manually") + } + .foregroundStyle(.secondary) + } + .disabled(true) + Spacer() + Button { + withAnimation(.linear(duration: 1)) { + effectTrigger.toggle() + } completion: { + effectTrigger.toggle() + } + extensionManager.checkIfExtensionsAreInstalled() + } label: { + HStack(spacing: 3) { + Image(systemName: "arrow.triangle.2.circlepath") + .rotationEffect(effectTrigger ? .degrees(360) : .zero) + } + .foregroundStyle(.secondary) + } + } + .controlSize(.small) + .buttonStyle(PlainButtonStyle()) + .overlay { + if extensionManager.installedExtensions.isEmpty { + Text("No extension installed") + .foregroundStyle(Color(.secondaryLabelColor)) + .padding(.bottom, 22) + } + } + } header: { + HStack(spacing: 0) { + Text("Installed extensions") + if !extensionManager.installedExtensions.isEmpty { + Text(" – \(extensionManager.installedExtensions.count)") + .foregroundStyle(.secondary) + } + } + } + } + .navigationTitle("Extensions") + // TipsView() + // .padding(.horizontal, 19) + } +} + +struct Appearance: View { + @ObservedObject var coordinator = DynamicIslandViewCoordinator.shared + @Default(.mirrorShape) var mirrorShape + @Default(.sliderColor) var sliderColor + @Default(.useMusicVisualizer) var useMusicVisualizer + @Default(.customVisualizers) var customVisualizers + @Default(.selectedVisualizer) var selectedVisualizer + let icons: [String] = ["logo2"] + @State private var selectedIcon: String = "logo2" + @State private var selectedListVisualizer: CustomVisualizer? = nil + + @State private var isPresented: Bool = false + @State private var name: String = "" + @State private var url: String = "" + @State private var speed: CGFloat = 1.0 + var body: some View { + Form { + Section { + Toggle("Always show tabs", isOn: $coordinator.alwaysShowTabs) + Defaults.Toggle("Settings icon in notch", key: .settingsIconInNotch) + Defaults.Toggle("Enable window shadow", key: .enableShadow) + Defaults.Toggle("Corner radius scaling", key: .cornerRadiusScaling) + Defaults.Toggle("Use simpler close animation", key: .useModernCloseAnimation) + } header: { + Text("General") + } + + Section { + Defaults.Toggle("Enable colored spectrograms", key: .coloredSpectrogram) + Defaults + .Toggle("Player tinting", key: .playerColorTinting) + Defaults.Toggle("Enable blur effect behind album art", key: .lightingEffect) + Picker("Slider color", selection: $sliderColor) { + ForEach(SliderColorEnum.allCases, id: \.self) { option in + Text(option.rawValue) + } + } + } header: { + Text("Media") + } + + Section { + Toggle( + "Use music visualizer spectrogram", + isOn: $useMusicVisualizer.animation() + ) + .disabled(true) + if !useMusicVisualizer { + if customVisualizers.count > 0 { + Picker( + "Selected animation", + selection: $selectedVisualizer + ) { + ForEach( + customVisualizers, + id: \.self + ) { visualizer in + Text(visualizer.name) + .tag(visualizer) + } + } + } else { + HStack { + Text("Selected animation") + Spacer() + Text("No custom animation available") + .foregroundStyle(.secondary) + } + } + } + } header: { + HStack { + Text("Custom music live activity animation") + customBadge(text: "Coming soon") + } + } + + Section { + List { + ForEach(customVisualizers, id: \.self) { visualizer in + HStack { + LottieView(state: LUStateData(type: .loadedFrom(visualizer.url), speed: visualizer.speed, loopMode: .loop)) + .frame(width: 30, height: 30, alignment: .center) + Text(visualizer.name) + Spacer(minLength: 0) + if selectedVisualizer == visualizer { + Text("selected") + .font(.caption) + .fontWeight(.medium) + .foregroundStyle(.secondary) + .padding(.trailing, 8) + } + } + .buttonStyle(PlainButtonStyle()) + .padding(.vertical, 2) + .background( + selectedListVisualizer != nil ? selectedListVisualizer == visualizer ? Color.accentColor : Color.clear : Color.clear, + in: RoundedRectangle(cornerRadius: 5) + ) + .contentShape(Rectangle()) + .onTapGesture { + if selectedListVisualizer == visualizer { + selectedListVisualizer = nil + return + } + selectedListVisualizer = visualizer + } + } + } + .safeAreaPadding( + EdgeInsets(top: 5, leading: 0, bottom: 5, trailing: 0) + ) + .frame(minHeight: 120) + .actionBar { + HStack(spacing: 5) { + Button { + name = "" + url = "" + speed = 1.0 + isPresented.toggle() + } label: { + Image(systemName: "plus") + .foregroundStyle(.secondary) + .contentShape(Rectangle()) + } + Divider() + Button { + if selectedListVisualizer != nil { + let visualizer = selectedListVisualizer! + selectedListVisualizer = nil + customVisualizers.remove(at: customVisualizers.firstIndex(of: visualizer)!) + if visualizer == selectedVisualizer && customVisualizers.count > 0 { + selectedVisualizer = customVisualizers[0] + } + } + } label: { + Image(systemName: "minus") + .foregroundStyle(.secondary) + .contentShape(Rectangle()) + } + } + } + .controlSize(.small) + .buttonStyle(PlainButtonStyle()) + .overlay { + if customVisualizers.isEmpty { + Text("No custom visualizer") + .foregroundStyle(Color(.secondaryLabelColor)) + .padding(.bottom, 22) + } + } + .sheet(isPresented: $isPresented) { + VStack(alignment: .leading) { + Text("Add new visualizer") + .font(.largeTitle.bold()) + .padding(.vertical) + TextField("Name", text: $name) + TextField("Lottie JSON URL", text: $url) + HStack { + Text("Speed") + Spacer(minLength: 80) + Text("\(speed, specifier: "%.1f")s") + .multilineTextAlignment(.trailing) + .foregroundStyle(.secondary) + Slider(value: $speed, in: 0...2, step: 0.1) + } + .padding(.vertical) + HStack { + Button { + isPresented.toggle() + } label: { + Text("Cancel") + .frame(maxWidth: .infinity, alignment: .center) + } + + Button { + let visualizer: CustomVisualizer = .init( + UUID: UUID(), + name: name, + url: URL(string: url)!, + speed: speed + ) + + if !customVisualizers.contains(visualizer) { + customVisualizers.append(visualizer) + } + + isPresented.toggle() + } label: { + Text("Add") + .frame(maxWidth: .infinity, alignment: .center) + } + .buttonStyle(BorderedProminentButtonStyle()) + } + } + .textFieldStyle(RoundedBorderTextFieldStyle()) + .controlSize(.extraLarge) + .padding() + } + } header: { + HStack(spacing: 0) { + Text("Custom vizualizers (Lottie)") + if !Defaults[.customVisualizers].isEmpty { + Text(" – \(Defaults[.customVisualizers].count)") + .foregroundStyle(.secondary) + } + } + } + + Section { + Defaults.Toggle("Enable boring mirror", key: .showMirror) + .disabled(!checkVideoInput()) + Picker("Mirror shape", selection: $mirrorShape) { + Text("Circle") + .tag(MirrorShapeEnum.circle) + Text("Square") + .tag(MirrorShapeEnum.rectangle) + } + Defaults.Toggle("Show cool face animation while inactivity", key: .showNotHumanFace) + } header: { + HStack { + Text("Additional features") + } + } + + Section { + HStack { + ForEach(icons, id: \.self) { icon in + Spacer() + VStack { + Image(icon) + .resizable() + .frame(width: 80, height: 80) + .background( + RoundedRectangle(cornerRadius: 20, style: .circular) + .strokeBorder( + icon == selectedIcon ? Color.accentColor : .clear, + lineWidth: 2.5 + ) + ) + + Text("Default") + .fontWeight(.medium) + .font(.caption) + .foregroundStyle(icon == selectedIcon ? .white : .secondary) + .padding(.horizontal, 10) + .padding(.vertical, 3) + .background( + Capsule() + .fill(icon == selectedIcon ? Color.accentColor : .clear) + ) + } + .onTapGesture { + withAnimation { + selectedIcon = icon + } + NSApp.applicationIconImage = NSImage(named: icon) + } + Spacer() + } + } + .disabled(true) + } header: { + HStack { + Text("App icon") + customBadge(text: "Coming soon") + } + } + } + .navigationTitle("Appearance") + } + + func checkVideoInput() -> Bool { + if let _ = AVCaptureDevice.default(for: .video) { + return true + } + + return false + } +} + +struct Shortcuts: View { + var body: some View { + Form { + Section { + KeyboardShortcuts.Recorder("Toggle Sneak Peek:", name: .toggleSneakPeek) + } header: { + Text("Media") + } footer: { + Text("Sneak Peek shows the media title and artist under the notch for a few seconds.") + .multilineTextAlignment(.trailing) + .foregroundStyle(.secondary) + .font(.caption) + } + Section { + KeyboardShortcuts.Recorder("Toggle Notch Open:", name: .toggleNotchOpen) + } + Section { + KeyboardShortcuts.Recorder("Start Demo Timer:", name: .startDemoTimer) + } header: { + Text("Timer") + } footer: { + Text("Starts a 5-minute demo timer to test the timer live activity feature.") + .multilineTextAlignment(.trailing) + .foregroundStyle(.secondary) + .font(.caption) + } + } + .navigationTitle("Shortcuts") + } +} + +struct Stats: View { + @ObservedObject var statsManager = StatsManager.shared + @Default(.enableStatsFeature) var enableStatsFeature + @Default(.showCpuGraph) var showCpuGraph + @Default(.showMemoryGraph) var showMemoryGraph + @Default(.showGpuGraph) var showGpuGraph + @Default(.showNetworkGraph) var showNetworkGraph + @Default(.showDiskGraph) var showDiskGraph + + var enabledGraphsCount: Int { + [showCpuGraph, showMemoryGraph, showGpuGraph, showNetworkGraph, showDiskGraph].filter { $0 }.count + } + + var body: some View { + Form { + Section { + Defaults.Toggle("Enable system stats monitoring", key: .enableStatsFeature) + .onChange(of: enableStatsFeature) { _, newValue in + if newValue { + statsManager.startMonitoring() + } else { + statsManager.stopMonitoring() + } + } + } header: { + Text("General") + } footer: { + Text("When enabled, the Stats tab will display real-time system performance graphs. This feature requires system permissions and may use additional battery.") + .multilineTextAlignment(.trailing) + .foregroundStyle(.secondary) + .font(.caption) + } + + if enableStatsFeature { + Section { + Defaults.Toggle("CPU Usage", key: .showCpuGraph) + Defaults.Toggle("Memory Usage", key: .showMemoryGraph) + Defaults.Toggle("GPU Usage", key: .showGpuGraph) + Defaults.Toggle("Network Activity", key: .showNetworkGraph) + Defaults.Toggle("Disk I/O", key: .showDiskGraph) + } header: { + Text("Graph Visibility") + } footer: { + if enabledGraphsCount >= 4 { + Text("With \(enabledGraphsCount) graphs enabled, the Dynamic Island will expand horizontally to accommodate all graphs in a single row.") + .multilineTextAlignment(.trailing) + .foregroundStyle(.secondary) + .font(.caption) + } else { + Text("Each graph can be individually enabled or disabled. Network activity shows download/upload speeds, and disk I/O shows read/write speeds.") + .multilineTextAlignment(.trailing) + .foregroundStyle(.secondary) + .font(.caption) + } + } + + Section { + HStack { + Text("Monitoring Status") + Spacer() + HStack(spacing: 4) { + Circle() + .fill(statsManager.isMonitoring ? .green : .red) + .frame(width: 8, height: 8) + Text(statsManager.isMonitoring ? "Active" : "Stopped") + .foregroundStyle(.secondary) + } + } + + if statsManager.isMonitoring { + if showCpuGraph { + HStack { + Text("CPU Usage") + Spacer() + Text(statsManager.cpuUsageString) + .foregroundStyle(.secondary) + } + } + + if showMemoryGraph { + HStack { + Text("Memory Usage") + Spacer() + Text(statsManager.memoryUsageString) + .foregroundStyle(.secondary) + } + } + + if showGpuGraph { + HStack { + Text("GPU Usage") + Spacer() + Text(statsManager.gpuUsageString) + .foregroundStyle(.secondary) + } + } + + if showNetworkGraph { + HStack { + Text("Network Download") + Spacer() + Text(String(format: "%.1f MB/s", statsManager.networkDownload)) + .foregroundStyle(.secondary) + } + + HStack { + Text("Network Upload") + Spacer() + Text(String(format: "%.1f MB/s", statsManager.networkUpload)) + .foregroundStyle(.secondary) + } + } + + if showDiskGraph { + HStack { + Text("Disk Read") + Spacer() + Text(String(format: "%.1f MB/s", statsManager.diskRead)) + .foregroundStyle(.secondary) + } + + HStack { + Text("Disk Write") + Spacer() + Text(String(format: "%.1f MB/s", statsManager.diskWrite)) + .foregroundStyle(.secondary) + } + } + + HStack { + Text("Last Updated") + Spacer() + Text(statsManager.lastUpdated, style: .relative) + .foregroundStyle(.secondary) + } + } + } header: { + Text("Live Performance Data") + } + + Section { + HStack { + Button(statsManager.isMonitoring ? "Stop Monitoring" : "Start Monitoring") { + if statsManager.isMonitoring { + statsManager.stopMonitoring() + } else { + statsManager.startMonitoring() + } + } + .buttonStyle(.borderedProminent) + .foregroundColor(statsManager.isMonitoring ? .red : .blue) + + Spacer() + + Button("Clear Data") { + statsManager.clearHistory() + } + .buttonStyle(.bordered) + .disabled(statsManager.isMonitoring) + } + } header: { + Text("Controls") + } + } + } + .navigationTitle("Stats") + } +} + +func proFeatureBadge() -> some View { + Text("Upgrade to Pro") + .foregroundStyle(Color(red: 0.545, green: 0.196, blue: 0.98)) + .font(.footnote.bold()) + .padding(.vertical, 3) + .padding(.horizontal, 6) + .background(RoundedRectangle(cornerRadius: 4).stroke(Color(red: 0.545, green: 0.196, blue: 0.98), lineWidth: 1)) +} + +func comingSoonTag() -> some View { + Text("Coming soon") + .foregroundStyle(.secondary) + .font(.footnote.bold()) + .padding(.vertical, 3) + .padding(.horizontal, 6) + .background(Color(nsColor: .secondarySystemFill)) + .clipShape(.capsule) +} + +func customBadge(text: String) -> some View { + Text(text) + .foregroundStyle(.secondary) + .font(.footnote.bold()) + .padding(.vertical, 3) + .padding(.horizontal, 6) + .background(Color(nsColor: .secondarySystemFill)) + .clipShape(.capsule) +} + +func warningBadge(_ text: String, _ description: String) -> some View { + Section { + HStack(spacing: 12) { + Image(systemName: "exclamationmark.triangle.fill") + .font(.system(size: 22)) + .foregroundStyle(.yellow) + VStack(alignment: .leading) { + Text(text) + .font(.headline) + Text(description) + .foregroundStyle(.secondary) + } + Spacer() + } + } +} + +struct ClipboardSettings: View { + @ObservedObject var clipboardManager = ClipboardManager.shared + @Default(.enableClipboardManager) var enableClipboardManager + @Default(.clipboardHistorySize) var clipboardHistorySize + @Default(.showClipboardIcon) var showClipboardIcon + + var body: some View { + Form { + Section { + Defaults.Toggle("Enable Clipboard Manager", key: .enableClipboardManager) + .onChange(of: enableClipboardManager) { enabled in + if enabled { + clipboardManager.startMonitoring() + } else { + clipboardManager.stopMonitoring() + } + } + } header: { + Text("Clipboard Manager") + } footer: { + Text("Monitor clipboard changes and keep a history of recent copies. Use Cmd+Shift+V to quickly access clipboard history.") + } + + if enableClipboardManager { + Section { + Defaults.Toggle("Show Clipboard Icon", key: .showClipboardIcon) + + HStack { + Text("History Size") + Spacer() + Picker("History Size", selection: $clipboardHistorySize) { + Text("3 items").tag(3) + Text("5 items").tag(5) + Text("7 items").tag(7) + Text("10 items").tag(10) + } + .pickerStyle(.menu) + .frame(width: 100) + } + + HStack { + Text("Current Items") + Spacer() + Text("\(clipboardManager.clipboardHistory.count)") + .foregroundColor(.secondary) + } + + HStack { + Text("Monitoring Status") + Spacer() + Text(clipboardManager.isMonitoring ? "Active" : "Stopped") + .foregroundColor(clipboardManager.isMonitoring ? .green : .secondary) + } + } header: { + Text("Settings") + } footer: { + Text("The clipboard icon appears in the header next to the settings gear when enabled. Use the global shortcut Cmd+Shift+V or click the icon to access history.") + } + + Section { + Button("Clear Clipboard History") { + clipboardManager.clearHistory() + } + .foregroundColor(.red) + .disabled(clipboardManager.clipboardHistory.isEmpty) + } header: { + Text("Actions") + } footer: { + Text("This will permanently delete all stored clipboard history. The clipboard button is located to the left of the settings gear in the header.") + } + + if !clipboardManager.clipboardHistory.isEmpty { + Section { + ForEach(clipboardManager.clipboardHistory) { item in + VStack(alignment: .leading, spacing: 4) { + HStack { + Image(systemName: item.type.icon) + .foregroundColor(.blue) + .frame(width: 16) + Text(item.type.displayName) + .font(.caption) + .foregroundColor(.secondary) + Spacer() + Text(timeAgoString(from: item.timestamp)) + .font(.caption2) + .foregroundColor(.secondary) + } + Text(item.preview) + .font(.system(.body, design: .monospaced)) + .lineLimit(2) + } + .padding(.vertical, 2) + } + } header: { + Text("Current History") + } + } + } + } + .formStyle(.grouped) + .navigationTitle("Clipboard") + .onAppear { + if enableClipboardManager && !clipboardManager.isMonitoring { + clipboardManager.startMonitoring() + } + } + } + + private func timeAgoString(from date: Date) -> String { + let interval = Date().timeIntervalSince(date) + + if interval < 60 { + return "Just now" + } else if interval < 3600 { + let minutes = Int(interval / 60) + return "\(minutes)m ago" + } else if interval < 86400 { + let hours = Int(interval / 3600) + return "\(hours)h ago" + } else { + let days = Int(interval / 86400) + return "\(days)d ago" + } + } +} + +#Preview { + HUD() +} \ No newline at end of file