diff --git a/Amperfy/Amperfy.entitlements b/Amperfy/Amperfy.entitlements index 5bd6602e..c4fc65a9 100644 --- a/Amperfy/Amperfy.entitlements +++ b/Amperfy/Amperfy.entitlements @@ -2,10 +2,10 @@ - com.apple.developer.carplay-audio - com.apple.developer.siri + com.apple.developer.carplay-audio + com.apple.security.app-sandbox com.apple.security.network.client diff --git a/Amperfy/AppDelegate.swift b/Amperfy/AppDelegate.swift index 527863bc..bf7403f2 100644 --- a/Amperfy/AppDelegate.swift +++ b/Amperfy/AppDelegate.swift @@ -37,7 +37,7 @@ let defaultWindowActivityType = "amperfy.main" @main class AppDelegate: UIResponder, UIApplicationDelegate { - static let name = "Amperfy" + static let name = "Musify" static var version: String { (Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String) ?? "" } @@ -236,8 +236,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func restartByUser() { Task { await localNotificationManager.notifyDebugAndWait( - title: "Amperfy Restart", - body: "Tap to reopen Amperfy" + title: "Musify Restart", + body: "Tap to reopen Musify" ) stopForInit() // close Amperfy diff --git a/Amperfy/AppDelegateMainMenuExtension.swift b/Amperfy/AppDelegateMainMenuExtension.swift index c3740929..9525588d 100644 --- a/Amperfy/AppDelegateMainMenuExtension.swift +++ b/Amperfy/AppDelegateMainMenuExtension.swift @@ -48,6 +48,13 @@ extension AppDelegate { guard builder.system == .main else { return } #if targetEnvironment(macCatalyst) // ok + // Replace About menu with custom Musify version info + builder.replace(menu: .about, with: UIMenu(options: .displayInline, children: [ + UIAction(title: "About Musify") { _ in + self.showCustomAboutPanel() + }, + ])) + // Add File menu let fileMenus = [ UIMenu(options: .displayInline, children: [ @@ -104,13 +111,34 @@ extension AppDelegate { builder.insertChild(UIMenu(options: .displayInline, children: [ UIAction(title: "Report an issue on GitHub") { _ in - if let url = URL(string: "https://github.com/BLeeEZ/amperfy/issues") { + if let url = URL(string: "https://github.com/DCM4711/amperfy/issues") { UIApplication.shared.open(url) } }, ]), atStartOfMenu: .help) } + private func showCustomAboutPanel() { + let message = """ + Version \(musifyVersion) + Based on Amperfy Version \(AppDelegate.version) (Build \(AppDelegate.buildNumber)) + + © 2026 DonkeyCat GmbH + A fork of Amperfy by Maximilian Bauer + Licensed under GPLv3 + """ + let alert = UIAlertController(title: "Musify", message: message, preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "OK", style: .default)) + + if let windowScene = UIApplication.shared.connectedScenes + .first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene, + let rootVC = windowScene.windows.first?.rootViewController { + var topVC = rootVC + while let presented = topVC.presentedViewController { topVC = presented } + topVC.present(alert, animated: true) + } + } + @objc private func keyCommandPause() { player.pause() diff --git a/Amperfy/AppIntentVocabulary.plist b/Amperfy/AppIntentVocabulary.plist index fb9128fa..268f52f5 100644 --- a/Amperfy/AppIntentVocabulary.plist +++ b/Amperfy/AppIntentVocabulary.plist @@ -9,9 +9,9 @@ INPlayMediaIntent IntentExamples - Play the example band in Amperfy - Play playlist example in Amperfy - Play title by artist in Amperfy + Play the example band in Musify + Play playlist example in Musify + Play title by artist in Musify diff --git a/Amperfy/CarPlay/CarPlayNowPlayingExtension.swift b/Amperfy/CarPlay/CarPlayNowPlayingExtension.swift index 2a098fe9..ab3ee6f0 100644 --- a/Amperfy/CarPlay/CarPlayNowPlayingExtension.swift +++ b/Amperfy/CarPlay/CarPlayNowPlayingExtension.swift @@ -33,6 +33,17 @@ extension CarPlaySceneDelegate { playerQueueSection.updateSections(createPlayerQueueSections()) } + func installArtworkStarOverlay() { + AmperKit.shared.nowPlayingInfoCenterHandler?.artworkTransform = { artwork, playable in + guard let song = playable.asSong else { return artwork } + return CarPlaySceneDelegate.overlayStarRating(on: artwork, rating: song.rating) + } + } + + func removeArtworkStarOverlay() { + AmperKit.shared.nowPlayingInfoCenterHandler?.artworkTransform = nil + } + func configureNowPlayingTemplate() { var buttons: [CPNowPlayingButton] = [] buttons.append( @@ -50,25 +61,29 @@ extension CarPlaySceneDelegate { ) if let currentlyPlaying = appDelegate.player.currentlyPlaying, !currentlyPlaying.isRadio { - let isFavorite = appDelegate.player.currentlyPlaying?.isFavorite ?? false + let currentRating = currentlyPlaying.asSong?.rating ?? 0 + let starImage = UIImage(systemName: currentRating > 0 ? "star.fill" : "star")! buttons.append( CPNowPlayingImageButton( - image: isFavorite ? .heartFill : .heartEmpty, + image: starImage, handler: { [weak self] button in guard let self = self else { return } - guard let playableInfo = appDelegate.player.currentlyPlaying, - let account = playableInfo.account else { return } + guard let song = appDelegate.player.currentlyPlaying?.asSong, + let account = song.account else { return } + let newRating = (song.rating + 1) % 6 + song.rating = newRating Task { @MainActor in do { - try await playableInfo - .remoteToggleFavorite( - syncer: self.appDelegate - .getMeta(account.info).librarySyncer - ) + try await self.appDelegate.getMeta(account.info) + .librarySyncer.setRating(song: song, rating: newRating) } catch { - self.appDelegate.eventLogger.report(topic: "Toggle Favorite", error: error) + self.appDelegate.eventLogger.report(topic: "Song Rating", error: error) } self.configureNowPlayingTemplate() + // Refresh artwork to update star overlay + if let playable = self.appDelegate.player.currentlyPlaying { + AmperKit.shared.nowPlayingInfoCenterHandler?.refreshNowPlayingInfo(playable: playable) + } } } ) @@ -93,13 +108,61 @@ extension CarPlaySceneDelegate { CPListSection(items: availablePlaybackRates), ]) interfaceController?.pushTemplate(playbackRateTemplate, animated: true, completion: nil) - }) ) CPNowPlayingTemplate.shared.updateNowPlayingButtons(buttons) CPNowPlayingTemplate.shared.isUpNextButtonEnabled = true } + /// Composites a star rating bar at the bottom of the artwork image. + static func overlayStarRating(on artwork: UIImage, rating: Int) -> UIImage { + let artworkSize = artwork.size + guard artworkSize.width > 0, artworkSize.height > 0 else { return artwork } + + let starCount = 5 + let starPointSize: CGFloat = artworkSize.width * 0.07 + let starSpacing: CGFloat = starPointSize * 0.4 + let bottomPadding: CGFloat = artworkSize.height * 0.03 + + let config = UIImage.SymbolConfiguration(pointSize: starPointSize, weight: .medium) + let filledStar = UIImage(systemName: "star.fill")!.withConfiguration(config) + let emptyStar = UIImage(systemName: "star")!.withConfiguration(config) + let starSize = filledStar.size + + let totalStarsWidth = CGFloat(starCount) * starSize.width + + CGFloat(starCount - 1) * starSpacing + let starsX = (artworkSize.width - totalStarsWidth) / 2 + let starsY = artworkSize.height - starSize.height - bottomPadding + + let renderer = UIGraphicsImageRenderer(size: artworkSize) + return renderer.image { ctx in + artwork.draw(in: CGRect(origin: .zero, size: artworkSize)) + + let bgRect = CGRect( + x: 0, + y: starsY - bottomPadding, + width: artworkSize.width, + height: starSize.height + bottomPadding * 2 + ) + UIColor.black.withAlphaComponent(0.90).setFill() + ctx.fill(bgRect) + + let goldenYellow = UIColor(red: 1.0, green: 0.84, blue: 0.0, alpha: 1.0) + let paleWhite = UIColor(white: 1.0, alpha: 0.3) + + for i in 0.. ())) { configureNowPlayingTemplate() if immediately { @@ -196,7 +259,7 @@ extension CarPlaySceneDelegate: CPNowPlayingTemplateObserver { ) listItem.handler = { [weak self] item, completion in guard let self = self else { completion(); return } - appDelegate.player.play(playerIndex: playerIndex) + appDelegate.player.play(playerIndex: playerIndex, autoStartPlayback: nil) Task { @MainActor in guard let _ = try? await interfaceController?.popTemplate(animated: true) else { return } completion() diff --git a/Amperfy/CarPlay/CarPlaySceneDelegate.swift b/Amperfy/CarPlay/CarPlaySceneDelegate.swift index 8ef7b1ab..9fae816f 100644 --- a/Amperfy/CarPlay/CarPlaySceneDelegate.swift +++ b/Amperfy/CarPlay/CarPlaySceneDelegate.swift @@ -111,6 +111,7 @@ class CarPlaySceneDelegate: UIResponder, CPTemplateApplicationSceneDelegate { CPNowPlayingTemplate.shared.add(self) self.interfaceController = interfaceController self.interfaceController?.delegate = self + self.installArtworkStarOverlay() self.configureNowPlayingTemplate() accountNotificationHandler? @@ -171,6 +172,7 @@ class CarPlaySceneDelegate: UIResponder, CPTemplateApplicationSceneDelegate { os_log("CarPlay: no account available -> do nothing", log: self.log, type: .info) return } + self.removeArtworkStarOverlay() self.interfaceController = nil appDelegate.notificationHandler.remove(self, name: .fetchControllerSortChanged, object: nil) appDelegate.notificationHandler.remove(self, name: .offlineModeChanged, object: nil) diff --git a/Amperfy/Info.plist b/Amperfy/Info.plist index f6fae8ec..010479bf 100644 --- a/Amperfy/Info.plist +++ b/Amperfy/Info.plist @@ -14,8 +14,10 @@ $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 + CFBundleDisplayName + Musify CFBundleName - $(PRODUCT_NAME) + Musify CFBundlePackageType APPL CFBundleShortVersionString @@ -39,9 +41,9 @@ INAlternativeAppName - Amperfy Music + Musify Music INAlternativeAppNamePronunciationHint - amperfy music + musify music INIntentsSupported diff --git a/Amperfy/Intents/IntentManager.swift b/Amperfy/Intents/IntentManager.swift index 5822caa1..6d689db3 100644 --- a/Amperfy/Intents/IntentManager.swift +++ b/Amperfy/Intents/IntentManager.swift @@ -619,7 +619,7 @@ public class IntentManager { documentation.append( XCallbackActionDocu( name: "SetOfflineMode", - description: "Sets the Amperfy offline mode to active/inactive", + description: "Sets the Musify offline mode to active/inactive", exampleURLs: [ "amperfy://x-callback-url/setOfflineMode?offlineMode=1", ], diff --git a/Amperfy/MiniPlayerSceneDelegate.swift b/Amperfy/MiniPlayerSceneDelegate.swift index bb1b77ff..56bd5014 100644 --- a/Amperfy/MiniPlayerSceneDelegate.swift +++ b/Amperfy/MiniPlayerSceneDelegate.swift @@ -50,6 +50,27 @@ class MiniPlayerSceneDelegate: UIResponder, UIWindowSceneDelegate { window?.rootViewController = PopupPlayerVC() window?.makeKeyAndVisible() + + applyMainWindowFrame(windowScene: windowScene) + } + + private func applyMainWindowFrame(windowScene: UIWindowScene) { + #if targetEnvironment(macCatalyst) + let defaults = UserDefaults.standard + let width = defaults.double(forKey: SceneDelegate.mainWindowFrameWidthKey) + let height = defaults.double(forKey: SceneDelegate.mainWindowFrameHeightKey) + guard width > 0, height > 0 else { return } + let x = defaults.double(forKey: SceneDelegate.mainWindowFrameXKey) + let y = defaults.double(forKey: SceneDelegate.mainWindowFrameYKey) + let frame = CGRect(x: x, y: y, width: width, height: height) + windowScene.requestGeometryUpdate(.Mac(systemFrame: frame)) + os_log( + "MiniPlayer: applied main window frame: %s", + log: self.log, + type: .info, + frame.debugDescription + ) + #endif } /** Called when the user activates your application by selecting a shortcut on the Home Screen, diff --git a/Amperfy/SceneDelegate.swift b/Amperfy/SceneDelegate.swift index 730e204a..4972b256 100644 --- a/Amperfy/SceneDelegate.swift +++ b/Amperfy/SceneDelegate.swift @@ -77,6 +77,11 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { static let mainWindowSize = CGSizeMake(1168, 688) // 2560 x 1600 #endif + static let mainWindowFrameXKey = "mainWindowFrameX" + static let mainWindowFrameYKey = "mainWindowFrameY" + static let mainWindowFrameWidthKey = "mainWindowFrameWidth" + static let mainWindowFrameHeightKey = "mainWindowFrameHeight" + public lazy var log = { AmperKit.shared.log }() @@ -123,6 +128,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { window?.makeKeyAndVisible() + restoreMainWindowFrame(windowScene: windowScene) + appDelegate.setAppAppearanceMode(style: appDelegate.storage.settings.user.appearanceMode) AmperfyAppShortcuts.updateAppShortcutParameters() } @@ -172,6 +179,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // Called when the scene will move from an active state to an inactive state. // This may occur due to temporary interruptions (ex. an incoming phone call). os_log("sceneWillResignActive", log: self.log, type: .info) + saveMainWindowFrame(scene: scene) guard appDelegate.isNormalInteraction else { return } @@ -192,6 +200,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // Save changes in the application's managed object context when the application transitions to the background. os_log("sceneDidEnterBackground", log: self.log, type: .info) + saveMainWindowFrame(scene: scene) AmperKit.shared.threadPerformanceMonitor.isInForeground = false guard appDelegate.isNormalInteraction else { return @@ -259,4 +268,34 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { func scene(_ scene: UIScene, didUpdate userActivity: NSUserActivity) { os_log("didUpdate userActivity: %s", log: self.log, type: .info, userActivity.activityType) } + + // MARK: - Window Frame Persistence + + private func saveMainWindowFrame(scene: UIScene) { + #if targetEnvironment(macCatalyst) + guard let windowScene = scene as? UIWindowScene else { return } + let frame = windowScene.effectiveGeometry.systemFrame + guard frame.width > 0, frame.height > 0 else { return } + let defaults = UserDefaults.standard + defaults.set(Double(frame.origin.x), forKey: Self.mainWindowFrameXKey) + defaults.set(Double(frame.origin.y), forKey: Self.mainWindowFrameYKey) + defaults.set(Double(frame.width), forKey: Self.mainWindowFrameWidthKey) + defaults.set(Double(frame.height), forKey: Self.mainWindowFrameHeightKey) + os_log("Saved main window frame: %s", log: self.log, type: .info, frame.debugDescription) + #endif + } + + private func restoreMainWindowFrame(windowScene: UIWindowScene) { + #if targetEnvironment(macCatalyst) + let defaults = UserDefaults.standard + let width = defaults.double(forKey: Self.mainWindowFrameWidthKey) + let height = defaults.double(forKey: Self.mainWindowFrameHeightKey) + guard width > 0, height > 0 else { return } + let x = defaults.double(forKey: Self.mainWindowFrameXKey) + let y = defaults.double(forKey: Self.mainWindowFrameYKey) + let frame = CGRect(x: x, y: y, width: width, height: height) + windowScene.requestGeometryUpdate(.Mac(systemFrame: frame)) + os_log("Restored main window frame: %s", log: self.log, type: .info, frame.debugDescription) + #endif + } } diff --git a/Amperfy/Screens/Basics/AnimatedGradientLayer.swift b/Amperfy/Screens/Basics/AnimatedGradientLayer.swift index 7ae53f10..705f4009 100644 --- a/Amperfy/Screens/Basics/AnimatedGradientLayer.swift +++ b/Amperfy/Screens/Basics/AnimatedGradientLayer.swift @@ -123,7 +123,8 @@ class PopupAnimatedGradientLayer { if inStyle == .dark { return [ coloredCornerColor.getWithLightness(of: lightnessDarkMode).cgColor, - UIColor.black.cgColor, + // 90% black (10% white) for custom dark mode + UIColor(white: 0.1, alpha: 1.0).cgColor, ] } else { return [ diff --git a/Amperfy/Screens/LaunchScreen.storyboard b/Amperfy/Screens/LaunchScreen.storyboard index 764c5351..b3c7638a 100644 --- a/Amperfy/Screens/LaunchScreen.storyboard +++ b/Amperfy/Screens/LaunchScreen.storyboard @@ -1,8 +1,8 @@ - - + + - + @@ -13,21 +13,22 @@ - + -