From f39d1aead39800b32bbda89d5574d1f36f85be2f Mon Sep 17 00:00:00 2001 From: eevee <94960726+whoeevee@users.noreply.github.com> Date: Sun, 2 Nov 2025 01:54:38 +0300 Subject: [PATCH] fixes and improvements --- .../DataLoaderServiceHooks.x.swift | 78 ++++++++---------- .../CustomLyrics+AllTracksLyrics.x.swift | 36 +++++++- .../CustomLyrics+DisableReportButton.x.swift | 25 +++++- .../Lyrics/CustomLyrics+HideOnError.x.swift | 32 ++++--- .../CustomLyrics+ScrollCrashFix.x.swift | 22 +++++ .../CustomLyrics+ShowAttributes.x.swift | 2 +- .../EeveeSpotify/Lyrics/CustomLyrics.x.swift | 25 ++++-- ...llDataSourceImplementation+Extension.swift | 12 +++ ...layingScrollViewController+Extension.swift | 32 +------ .../Headers/NPVScrollViewController.swift | 6 ++ ...layingScrollDataSourceImplementation.swift | 3 + .../StatefulPlayerImplementation.swift | 5 ++ .../Headers/__UIDiffableDataSource.swift | 6 ++ ...ngScrollViewControllerInstanceHook.x.swift | 50 +++++------ .../DynamicPremium+ModifyBootstrap.x.swift | 2 +- .../Premium/LikedSongsEnabler.x.swift | 2 +- .../Premium/ServerSidedReminder.x.swift | 34 ++++++-- .../Premium/SiriNoPlayAsRadio.x.swift | 2 +- .../Premium/TrackRowsEnabler.x.swift | 2 +- Sources/EeveeSpotify/Tweak.x.swift | 28 ++++++- .../resolveconfiguration.bnk | Bin 66047 -> 79724 bytes 21 files changed, 267 insertions(+), 137 deletions(-) create mode 100644 Sources/EeveeSpotify/Lyrics/CustomLyrics+ScrollCrashFix.x.swift create mode 100644 Sources/EeveeSpotify/Lyrics/Models/Extensions/NowPlayingScrollDataSourceImplementation+Extension.swift create mode 100644 Sources/EeveeSpotify/Lyrics/Models/Headers/NPVScrollViewController.swift create mode 100644 Sources/EeveeSpotify/Lyrics/Models/Headers/NowPlayingScrollDataSourceImplementation.swift create mode 100644 Sources/EeveeSpotify/Lyrics/Models/Headers/StatefulPlayerImplementation.swift create mode 100644 Sources/EeveeSpotify/Lyrics/Models/Headers/__UIDiffableDataSource.swift diff --git a/Sources/EeveeSpotify/DataLoaderServiceHooks.x.swift b/Sources/EeveeSpotify/DataLoaderServiceHooks.x.swift index 831a1db..e6995c3 100644 --- a/Sources/EeveeSpotify/DataLoaderServiceHooks.x.swift +++ b/Sources/EeveeSpotify/DataLoaderServiceHooks.x.swift @@ -6,8 +6,8 @@ class SPTDataLoaderServiceHook: ClassHook, SpotifySessionDelegate { // orion:new func shouldModify(_ url: URL) -> Bool { - let shouldPatchPremium = PremiumPatchingGroup.isActive - let shouldReplaceLyrics = LyricsGroup.isActive + let shouldPatchPremium = BasePremiumPatchingGroup.isActive + let shouldReplaceLyrics = BaseLyricsGroup.isActive return (shouldReplaceLyrics && url.isLyrics) || (shouldPatchPremium && (url.isCustomize || url.isPremiumPlanRow || url.isPremiumBadge || url.isPlanOverview)) @@ -33,61 +33,53 @@ class SPTDataLoaderServiceHook: ClassHook, SpotifySessionDelegate { return } + guard let buffer = URLSessionHelper.shared.obtainData(for: url) else { + return + } + do { - if let buffer = URLSessionHelper.shared.obtainData(for: url) { - if url.isLyrics { - respondWithCustomData( - try getLyricsDataForCurrentTrack( - originalLyrics: try? Lyrics(serializedBytes: buffer) - ), - task: task, - session: session - ) - - return - } - - if url.isPremiumPlanRow { - respondWithCustomData( - try getPremiumPlanRowData( - originalPremiumPlanRow: try PremiumPlanRow(serializedBytes: buffer) - ), - task: task, - session: session - ) - - return - } - - if url.isPremiumBadge { - respondWithCustomData(try getPremiumPlanBadge(), task: task, session: session) - return - } - + if url.isLyrics { + respondWithCustomData( + try getLyricsDataForCurrentTrack( + originalLyrics: try? Lyrics(serializedBytes: buffer) + ), + task: task, + session: session + ) + return + } + + if url.isPremiumPlanRow { + respondWithCustomData( + try getPremiumPlanRowData( + originalPremiumPlanRow: try PremiumPlanRow(serializedBytes: buffer) + ), + task: task, + session: session + ) + return + } + + if url.isPremiumBadge { + respondWithCustomData(try getPremiumPlanBadge(), task: task, session: session) + return + } + + if url.isCustomize { var customizeMessage = try CustomizeMessage(serializedBytes: buffer) modifyRemoteConfiguration(&customizeMessage.response) - respondWithCustomData(try customizeMessage.serializedData(), task: task, session: session) return } if url.isPlanOverview { - do { - orig.URLSession(session, dataTask: task, didReceiveData: try getPlanOverviewData()) - orig.URLSession(session, task: task, didCompleteWithError: nil) - } - catch { - orig.URLSession(session, task: task, didCompleteWithError: error) - } - + respondWithCustomData(try getPlanOverviewData(), task: task, session: session) return } } catch { orig.URLSession(session, task: task, didCompleteWithError: error) } - - orig.URLSession(session, task: task, didCompleteWithError: error) } func URLSession( diff --git a/Sources/EeveeSpotify/Lyrics/CustomLyrics+AllTracksLyrics.x.swift b/Sources/EeveeSpotify/Lyrics/CustomLyrics+AllTracksLyrics.x.swift index 8541758..6e954ef 100644 --- a/Sources/EeveeSpotify/Lyrics/CustomLyrics+AllTracksLyrics.x.swift +++ b/Sources/EeveeSpotify/Lyrics/CustomLyrics+AllTracksLyrics.x.swift @@ -1,22 +1,35 @@ import Orion import UIKit +private var shouldOverrideLocalTrackURI = false + class SPTPlayerTrackHook: ClassHook { - typealias Group = LyricsGroup + typealias Group = BaseLyricsGroup static let targetName = EeveeSpotify.hookTarget == .latest ? "SPTPlayerTrackImplementation" : "SPTPlayerTrack" func metadata() -> [String: String] { var meta = orig.metadata() - meta["has_lyrics"] = "true" return meta } + + func URI() -> NSURL? { + let uri = orig.URI() + + guard shouldOverrideLocalTrackURI, + let absoluteString = uri?.absoluteString, + absoluteString.hasPrefix("spotify:local:") else { + return uri + } + + return NSURL(string: "spotify:track:")! + } } class LyricsScrollProviderHook: ClassHook { - typealias Group = LyricsGroup + typealias Group = BaseLyricsGroup static var targetName = HookTargetNameHelper.lyricsScrollProvider func isEnabledForTrack(_ track: SPTPlayerTrack) -> Bool { @@ -24,8 +37,23 @@ class LyricsScrollProviderHook: ClassHook { } } +class NPVScrollViewControllerHook: ClassHook { + typealias Group = ModernLyricsGroup + static var targetName = "NowPlaying_ScrollImpl.NPVScrollViewController" + + func viewWillAppear(_ animated: Bool) { + shouldOverrideLocalTrackURI = true + orig.viewWillAppear(animated) + } + + func viewWillDisappear(_ animated: Bool) { + shouldOverrideLocalTrackURI = false + orig.viewWillDisappear(animated) + } +} + class NowPlayingScrollViewControllerHook: ClassHook { - typealias Group = LyricsGroup + typealias Group = LegacyLyricsGroup static var targetName = "NowPlaying_ScrollImpl.NowPlayingScrollViewController" func nowPlayingScrollViewModelWithDidLoadComponentsFor( diff --git a/Sources/EeveeSpotify/Lyrics/CustomLyrics+DisableReportButton.x.swift b/Sources/EeveeSpotify/Lyrics/CustomLyrics+DisableReportButton.x.swift index b3cad13..f10d124 100644 --- a/Sources/EeveeSpotify/Lyrics/CustomLyrics+DisableReportButton.x.swift +++ b/Sources/EeveeSpotify/Lyrics/CustomLyrics+DisableReportButton.x.swift @@ -2,12 +2,13 @@ import Orion import UIKit class LyricsFullscreenViewControllerHook: ClassHook { - typealias Group = LyricsGroup + typealias Group = BaseLyricsGroup static var targetName: String { switch EeveeSpotify.hookTarget { case .lastAvailableiOS14: return "Lyrics_CoreImpl.FullscreenViewController" - default: return "Lyrics_FullscreenPageImpl.FullscreenViewController" + case .lastAvailableiOS15: return "Lyrics_FullscreenPageImpl.FullscreenViewController" + default: return "Lyrics_FullscreenElementPageImpl.FullscreenElementViewController" } } @@ -21,6 +22,26 @@ class LyricsFullscreenViewControllerHook: ClassHook { return } + if EeveeSpotify.hookTarget == .latest { + guard let fullscreenView = WindowHelper.shared.findFirstSubview( + "Lyrics_FullscreenElementPageImpl.FullscreenView", + in: target.view + ) else { + return + } + + let controlsView = Ivars(fullscreenView).controlsView + let contextMenuButtonContainer = Ivars(controlsView).contextMenuButtonContainer + + if let contextButton = contextMenuButtonContainer.subviews( + matching: "Encore6Button" + ).first as? UIControl { + contextButton.isEnabled = false + } + + return + } + let headerView = Ivars(target.view).headerView if let reportButton = headerView.subviews(matching: "EncoreButton")[1] as? UIButton { diff --git a/Sources/EeveeSpotify/Lyrics/CustomLyrics+HideOnError.x.swift b/Sources/EeveeSpotify/Lyrics/CustomLyrics+HideOnError.x.swift index edb3fd7..5fbe90c 100644 --- a/Sources/EeveeSpotify/Lyrics/CustomLyrics+HideOnError.x.swift +++ b/Sources/EeveeSpotify/Lyrics/CustomLyrics+HideOnError.x.swift @@ -2,7 +2,7 @@ import Orion import UIKit class ErrorViewControllerHook: ClassHook { - typealias Group = LyricsGroup + typealias Group = BaseLyricsGroup static var targetName: String { switch EeveeSpotify.hookTarget { @@ -14,17 +14,29 @@ class ErrorViewControllerHook: ClassHook { func loadView() { orig.loadView() - guard UserDefaults.lyricsOptions.hideOnError, let controller = nowPlayingScrollViewController else { + guard UserDefaults.lyricsOptions.hideOnError else { return } - var providers = controller.activeProviders - - providers.removeAll( - where: { NSStringFromClass(type(of: $0)) == HookTargetNameHelper.lyricsScrollProvider } - ) - - controller.activeProviders = providers - controller.collectionView().reloadData() + if let controller = nowPlayingScrollViewController { + controller.dataSource.activeProviders.removeAll { + NSStringFromClass(type(of: $0)) == HookTargetNameHelper.lyricsScrollProvider + } + + controller.collectionView().reloadData() + } + else if let controller = npvScrollViewController, let dataSource = scrollDataSource { + let lyricsProviderIndex = dataSource.activeProviders.firstIndex { + NSStringFromClass(type(of: $0)) == HookTargetNameHelper.lyricsScrollProvider + } + + let collectionView = controller.collectionView() + let dataSource = Ivars<__UIDiffableDataSource>(collectionView.dataSource!)._impl + + let itemIdentifiers = dataSource.itemIdentifiers() + let lyricsProviderItemIdentifier = itemIdentifiers[lyricsProviderIndex!] + + dataSource.deleteItemsWithIdentifiers([lyricsProviderItemIdentifier]) + } } } diff --git a/Sources/EeveeSpotify/Lyrics/CustomLyrics+ScrollCrashFix.x.swift b/Sources/EeveeSpotify/Lyrics/CustomLyrics+ScrollCrashFix.x.swift new file mode 100644 index 0000000..fce8d72 --- /dev/null +++ b/Sources/EeveeSpotify/Lyrics/CustomLyrics+ScrollCrashFix.x.swift @@ -0,0 +1,22 @@ +import UIKit +import Orion + +class UITableViewHook: ClassHook { + typealias Group = BaseLyricsGroup + + func scrollToRowAtIndexPath( + _ indexPath: NSIndexPath, + atScrollPosition scrollPosition: UITableView.ScrollPosition, + animated: Bool + ) { + if target.numberOfRows(inSection: indexPath.section) == 0 { + return + } + + orig.scrollToRowAtIndexPath( + indexPath, + atScrollPosition: scrollPosition, + animated: animated + ) + } +} diff --git a/Sources/EeveeSpotify/Lyrics/CustomLyrics+ShowAttributes.x.swift b/Sources/EeveeSpotify/Lyrics/CustomLyrics+ShowAttributes.x.swift index 2d56d9a..7d1280c 100644 --- a/Sources/EeveeSpotify/Lyrics/CustomLyrics+ShowAttributes.x.swift +++ b/Sources/EeveeSpotify/Lyrics/CustomLyrics+ShowAttributes.x.swift @@ -2,7 +2,7 @@ import Orion import UIKit class LyricsOnlyViewControllerHook: ClassHook { - typealias Group = LyricsGroup + typealias Group = BaseLyricsGroup static var targetName: String { switch EeveeSpotify.hookTarget { diff --git a/Sources/EeveeSpotify/Lyrics/CustomLyrics.x.swift b/Sources/EeveeSpotify/Lyrics/CustomLyrics.x.swift index 6be1b3d..4f662b4 100644 --- a/Sources/EeveeSpotify/Lyrics/CustomLyrics.x.swift +++ b/Sources/EeveeSpotify/Lyrics/CustomLyrics.x.swift @@ -3,7 +3,10 @@ import SwiftUI // -struct LyricsGroup: HookGroup { } +struct BaseLyricsGroup: HookGroup { } + +struct LegacyLyricsGroup: HookGroup { } +struct ModernLyricsGroup: HookGroup { } var lyricsState = LyricsLoadingState() @@ -16,9 +19,12 @@ private let petitLyricsRepository = PetitLyricsRepository() // private func loadCustomLyricsForCurrentTrack() throws -> Lyrics { - guard let track = nowPlayingScrollViewController?.loadedTrack else { - throw LyricsError.noCurrentTrack - } + guard + let track = statefulPlayer?.currentTrack() ?? + nowPlayingScrollViewController?.loadedTrack + else { + throw LyricsError.noCurrentTrack + } let searchQuery = LyricsSearchQuery( title: track.trackTitle(), @@ -115,9 +121,12 @@ private func loadCustomLyricsForCurrentTrack() throws -> Lyrics { } func getLyricsDataForCurrentTrack(originalLyrics: Lyrics? = nil) throws -> Data { - guard let track = nowPlayingScrollViewController?.loadedTrack else { - throw LyricsError.noCurrentTrack - } + guard + let track = statefulPlayer?.currentTrack() ?? + nowPlayingScrollViewController?.loadedTrack + else { + throw LyricsError.noCurrentTrack + } var lyrics = try loadCustomLyricsForCurrentTrack() @@ -143,7 +152,7 @@ func getLyricsDataForCurrentTrack(originalLyrics: Lyrics? = nil) throws -> Data color = Color(hex: extractedColor) .normalized(lyricsColorsSettings.normalizationFactor) } - else if let uiColor = nowPlayingScrollViewController?.backgroundViewModel.color() { + else if let uiColor = backgroundViewModel?.color() { color = Color(uiColor) .normalized(lyricsColorsSettings.normalizationFactor) } diff --git a/Sources/EeveeSpotify/Lyrics/Models/Extensions/NowPlayingScrollDataSourceImplementation+Extension.swift b/Sources/EeveeSpotify/Lyrics/Models/Extensions/NowPlayingScrollDataSourceImplementation+Extension.swift new file mode 100644 index 0000000..b3025d7 --- /dev/null +++ b/Sources/EeveeSpotify/Lyrics/Models/Extensions/NowPlayingScrollDataSourceImplementation+Extension.swift @@ -0,0 +1,12 @@ +import Orion + +extension NowPlayingScrollDataSourceImplementation { + var activeProviders: Array { + get { + Ivars>(self).activeProviders + } + set { + Ivars>(self).activeProviders = newValue + } + } +} diff --git a/Sources/EeveeSpotify/Lyrics/Models/Extensions/NowPlayingScrollViewController+Extension.swift b/Sources/EeveeSpotify/Lyrics/Models/Extensions/NowPlayingScrollViewController+Extension.swift index 5518b53..819a20b 100644 --- a/Sources/EeveeSpotify/Lyrics/Models/Extensions/NowPlayingScrollViewController+Extension.swift +++ b/Sources/EeveeSpotify/Lyrics/Models/Extensions/NowPlayingScrollViewController+Extension.swift @@ -22,37 +22,9 @@ extension NowPlayingScrollViewController { } } - // - - private var dataSource: NSObject { + var dataSource: NowPlayingScrollDataSourceImplementation { get { - Ivars(nowPlayingScrollViewModel).dataSource - } - } - - var activeProviders: Array { - get { - Ivars>(dataSource).activeProviders - } - set { - Ivars>(dataSource).activeProviders = newValue - } - } - - // - - private var backgroundViewController: NSObject { - get { - Ivars(self).backgroundViewController - } - } - - var backgroundViewModel: SPTNowPlayingBackgroundViewModel { - get { - let ivars = Ivars(self.backgroundViewController) - return EeveeSpotify.hookTarget == .latest - ? ivars.artworkColorObservable - : ivars.viewModel + Ivars(nowPlayingScrollViewModel).dataSource } } } diff --git a/Sources/EeveeSpotify/Lyrics/Models/Headers/NPVScrollViewController.swift b/Sources/EeveeSpotify/Lyrics/Models/Headers/NPVScrollViewController.swift new file mode 100644 index 0000000..a83b2ff --- /dev/null +++ b/Sources/EeveeSpotify/Lyrics/Models/Headers/NPVScrollViewController.swift @@ -0,0 +1,6 @@ +import Foundation +import UIKit + +@objc protocol NPVScrollViewController { + func collectionView() -> UICollectionView +} diff --git a/Sources/EeveeSpotify/Lyrics/Models/Headers/NowPlayingScrollDataSourceImplementation.swift b/Sources/EeveeSpotify/Lyrics/Models/Headers/NowPlayingScrollDataSourceImplementation.swift new file mode 100644 index 0000000..b65c1db --- /dev/null +++ b/Sources/EeveeSpotify/Lyrics/Models/Headers/NowPlayingScrollDataSourceImplementation.swift @@ -0,0 +1,3 @@ +import Foundation + +@objc protocol NowPlayingScrollDataSourceImplementation { } diff --git a/Sources/EeveeSpotify/Lyrics/Models/Headers/StatefulPlayerImplementation.swift b/Sources/EeveeSpotify/Lyrics/Models/Headers/StatefulPlayerImplementation.swift new file mode 100644 index 0000000..1ab17a5 --- /dev/null +++ b/Sources/EeveeSpotify/Lyrics/Models/Headers/StatefulPlayerImplementation.swift @@ -0,0 +1,5 @@ +import Foundation + +@objc protocol StatefulPlayerImplementation { + func currentTrack() -> SPTPlayerTrack? +} diff --git a/Sources/EeveeSpotify/Lyrics/Models/Headers/__UIDiffableDataSource.swift b/Sources/EeveeSpotify/Lyrics/Models/Headers/__UIDiffableDataSource.swift new file mode 100644 index 0000000..3aa4eb7 --- /dev/null +++ b/Sources/EeveeSpotify/Lyrics/Models/Headers/__UIDiffableDataSource.swift @@ -0,0 +1,6 @@ +import UIKit + +@objc protocol __UIDiffableDataSource { + func itemIdentifiers() -> NSArray + func deleteItemsWithIdentifiers(_ identifiers: NSArray) +} diff --git a/Sources/EeveeSpotify/Lyrics/NowPlayingScrollViewControllerInstanceHook.x.swift b/Sources/EeveeSpotify/Lyrics/NowPlayingScrollViewControllerInstanceHook.x.swift index b863eb1..6de1e7c 100644 --- a/Sources/EeveeSpotify/Lyrics/NowPlayingScrollViewControllerInstanceHook.x.swift +++ b/Sources/EeveeSpotify/Lyrics/NowPlayingScrollViewControllerInstanceHook.x.swift @@ -1,39 +1,39 @@ import Orion import UIKit -var nowPlayingScrollViewController: NowPlayingScrollViewController? +var statefulPlayer: StatefulPlayerImplementation? +var backgroundViewModel: SPTNowPlayingBackgroundViewModel? +var scrollDataSource: NowPlayingScrollDataSourceImplementation? -class NowPlayingScrollViewControllerInstanceHook: ClassHook { - typealias Group = LyricsGroup - static let targetName = "NowPlaying_ScrollImpl.NowPlayingScrollViewController" - - func nowPlayingScrollViewModelWithDidMoveToRelativeTrack( - _ track: SPTPlayerTrack, - withDifferentProviders: Bool, - scrollEnabledValueChanged: Bool - ) -> NowPlayingScrollViewController { - nowPlayingScrollViewController = orig.nowPlayingScrollViewModelWithDidMoveToRelativeTrack( - track, - withDifferentProviders: withDifferentProviders, - scrollEnabledValueChanged: scrollEnabledValueChanged - ) - - return nowPlayingScrollViewController! - } -} +var nowPlayingScrollViewController: NowPlayingScrollViewController? +var npvScrollViewController: NPVScrollViewController? class NowPlayingScrollPrivateServiceImplementationHook: ClassHook { - typealias Group = LyricsGroup + typealias Group = BaseLyricsGroup static let targetName = "NowPlaying_ScrollImpl.NowPlayingScrollPrivateServiceImplementation" func provideScrollViewControllerWithDependencies(_ dependencies: NSObject) -> UIViewController { - // spotify introduced some "nova scroll" with different controllers and logic - // hope they don't remove backward compatibility, i don't want to rewrite ts 😭🙏 + let scrollViewController = orig.provideScrollViewControllerWithDependencies(dependencies) - if EeveeSpotify.hookTarget != .lastAvailableiOS14 { - Ivars(target).$__lazy_storage_$_isNovaScrollEnabled = false + if NSStringFromClass(type(of: scrollViewController)) ~= "NowPlayingScrollViewController" { + nowPlayingScrollViewController = Dynamic.convert( + scrollViewController, + to: NowPlayingScrollViewController.self + ) + } + else { + statefulPlayer = Ivars(dependencies).statefulPlayer + scrollDataSource = Ivars(target) + .$__lazy_storage_$_scrollDataSource + npvScrollViewController = Dynamic.convert( + scrollViewController, + to: NPVScrollViewController.self + ) } - return orig.provideScrollViewControllerWithDependencies(dependencies) + backgroundViewModel = Ivars(dependencies) + .backgroundViewModel + + return scrollViewController } } diff --git a/Sources/EeveeSpotify/Premium/DynamicPremium+ModifyBootstrap.x.swift b/Sources/EeveeSpotify/Premium/DynamicPremium+ModifyBootstrap.x.swift index 6a6d9a0..ba9775a 100644 --- a/Sources/EeveeSpotify/Premium/DynamicPremium+ModifyBootstrap.x.swift +++ b/Sources/EeveeSpotify/Premium/DynamicPremium+ModifyBootstrap.x.swift @@ -70,7 +70,7 @@ class SpotifySessionDelegateBootstrapHook: ClassHook, SpotifySessionDe } else { UserDefaults.patchType = .requests - PremiumPatchingGroup().activate() + activatePremiumPatchingGroup() } NSLog("[EeveeSpotify] Fetched bootstrap, \(UserDefaults.patchType) was set") diff --git a/Sources/EeveeSpotify/Premium/LikedSongsEnabler.x.swift b/Sources/EeveeSpotify/Premium/LikedSongsEnabler.x.swift index 661f8e9..0ac07f0 100644 --- a/Sources/EeveeSpotify/Premium/LikedSongsEnabler.x.swift +++ b/Sources/EeveeSpotify/Premium/LikedSongsEnabler.x.swift @@ -6,7 +6,7 @@ private let likedTracksRow: [String: Any] = [ ] class HUBViewModelBuilderImplementationHook: ClassHook { - typealias Group = PremiumPatchingGroup + typealias Group = BasePremiumPatchingGroup static let targetName: String = "HUBViewModelBuilderImplementation" func addJSONDictionary(_ dictionary: NSDictionary?) { diff --git a/Sources/EeveeSpotify/Premium/ServerSidedReminder.x.swift b/Sources/EeveeSpotify/Premium/ServerSidedReminder.x.swift index b5bb3f2..9b96ea2 100644 --- a/Sources/EeveeSpotify/Premium/ServerSidedReminder.x.swift +++ b/Sources/EeveeSpotify/Premium/ServerSidedReminder.x.swift @@ -1,22 +1,42 @@ import Orion import UIKit +private func showHighQualityPopUp() { + PopUpHelper.showPopUp( + message: "high_audio_quality_popup".localized, + buttonText: "OK".uiKitLocalized + ) +} + +class ListRowInteractionListenerViewHook: ClassHook { + typealias Group = ModernPremiumPatchingGroup + static let targetName = "_TtC15Settings_ECMKit30ListRowInteractionListenerView" + + func performAction() { + guard + let accessibilityLabel = target.subviews.first?.accessibilityLabel, + accessibilityLabel.hasSuffix("Premium") + else { + orig.performAction() + return + } + + showHighQualityPopUp() + } +} + class StreamQualitySettingsSectionHook: ClassHook { - typealias Group = PremiumPatchingGroup + typealias Group = LegacyPremiumPatchingGroup static let targetName = "StreamQualitySettingsSection" func shouldResetSelection() -> Bool { - PopUpHelper.showPopUp( - message: "high_audio_quality_popup".localized, - buttonText: "OK".uiKitLocalized - ) - + showHighQualityPopUp() return true } } class ContentOffliningUIHelperImplementationHook: ClassHook { - typealias Group = PremiumPatchingGroup + typealias Group = BasePremiumPatchingGroup static let targetName = "Offline_ContentOffliningUIImpl.ContentOffliningUIHelperImplementation" func downloadToggledWithCurrentAvailability( diff --git a/Sources/EeveeSpotify/Premium/SiriNoPlayAsRadio.x.swift b/Sources/EeveeSpotify/Premium/SiriNoPlayAsRadio.x.swift index 67984dc..5b34f46 100644 --- a/Sources/EeveeSpotify/Premium/SiriNoPlayAsRadio.x.swift +++ b/Sources/EeveeSpotify/Premium/SiriNoPlayAsRadio.x.swift @@ -2,7 +2,7 @@ import Orion import Intents class INMediaItemHook: ClassHook { - typealias Group = PremiumPatchingGroup + typealias Group = BasePremiumPatchingGroup func identifier() -> String { var identifier = orig.identifier() diff --git a/Sources/EeveeSpotify/Premium/TrackRowsEnabler.x.swift b/Sources/EeveeSpotify/Premium/TrackRowsEnabler.x.swift index f56c05a..aac8ad1 100644 --- a/Sources/EeveeSpotify/Premium/TrackRowsEnabler.x.swift +++ b/Sources/EeveeSpotify/Premium/TrackRowsEnabler.x.swift @@ -1,7 +1,7 @@ import Orion class SPTFreeTierArtistHubRemoteURLResolverHook: ClassHook { - typealias Group = PremiumPatchingGroup + typealias Group = BasePremiumPatchingGroup static let targetName = "SPTFreeTierArtistHubRemoteURLResolver" func initWithViewURI( diff --git a/Sources/EeveeSpotify/Tweak.x.swift b/Sources/EeveeSpotify/Tweak.x.swift index 6dc7be5..4026e0c 100644 --- a/Sources/EeveeSpotify/Tweak.x.swift +++ b/Sources/EeveeSpotify/Tweak.x.swift @@ -1,4 +1,5 @@ import Orion +import EeveeSpotifyC import UIKit func exitApplication() { @@ -8,7 +9,21 @@ func exitApplication() { } } -struct PremiumPatchingGroup: HookGroup { } +struct BasePremiumPatchingGroup: HookGroup { } + +struct LegacyPremiumPatchingGroup: HookGroup { } +struct ModernPremiumPatchingGroup: HookGroup { } + +func activatePremiumPatchingGroup() { + BasePremiumPatchingGroup().activate() + + if EeveeSpotify.hookTarget == .latest { + ModernPremiumPatchingGroup().activate() + } + else { + LegacyPremiumPatchingGroup().activate() + } +} struct EeveeSpotify: Tweak { static let version = "6.1.6" @@ -36,11 +51,18 @@ struct EeveeSpotify: Tweak { } if UserDefaults.patchType.isPatching { - PremiumPatchingGroup().activate() + activatePremiumPatchingGroup() } if UserDefaults.lyricsSource.isReplacingLyrics { - LyricsGroup().activate() + BaseLyricsGroup().activate() + + if EeveeSpotify.hookTarget == .latest { + ModernLyricsGroup().activate() + } + else { + LegacyLyricsGroup().activate() + } } } } diff --git a/layout/Library/Application Support/EeveeSpotify.bundle/resolveconfiguration.bnk b/layout/Library/Application Support/EeveeSpotify.bundle/resolveconfiguration.bnk index e81b7f143459cd155c6e36b39ba6738f0bbc4e17..8ede0a965cde7fa33560b1de3edcb04acba0dde8 100644 GIT binary patch delta 22346 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