mirror of
https://github.com/whoeevee/EeveeSpotifyReborn.git
synced 2026-01-09 00:23:20 +01:00
v1.4: fixed settings issue, implemented popup on data reload, blocked store review
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
import Orion
|
||||
import UIKit
|
||||
import StoreKit
|
||||
|
||||
// uncomment if you're building for jailbreak, cause ipa crashes:
|
||||
// Fatal error: Error in tweak EeveeSpotify: Failed to hook method -[SKStoreReviewController requestReview:] (Could not hook method)
|
||||
// Fatal error: Error in tweak EeveeSpotify: Failed to hook method -[SKStoreReviewController requestReviewInScene:] (Could not hook method)
|
||||
|
||||
// class SKStoreReviewControllerHook: ClassHook<SKStoreReviewController> {
|
||||
// func requestReview() { }
|
||||
// func requestReviewInScene(_ scene: UIWindowScene) { }
|
||||
// }
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
import Foundation
|
||||
|
||||
class OfflineHelper {
|
||||
|
||||
static let persistentCachePath = FileManager.default.urls(
|
||||
for: .applicationSupportDirectory, in: .userDomainMask
|
||||
)
|
||||
.first!
|
||||
.appendingPathComponent("PersistentCache")
|
||||
|
||||
//
|
||||
|
||||
static var offlineBnkPath: URL {
|
||||
persistentCachePath.appendingPathComponent("offline.bnk")
|
||||
}
|
||||
|
||||
static var eeveeBnkPath: URL {
|
||||
persistentCachePath.appendingPathComponent("eevee.bnk")
|
||||
}
|
||||
|
||||
static var offlineBnkData: Data {
|
||||
get throws { try Data(contentsOf: offlineBnkPath) }
|
||||
}
|
||||
|
||||
static var eeveeBnkData: Data {
|
||||
get throws { try Data(contentsOf: eeveeBnkPath) }
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
private static func writeOfflineBnkData(_ data: Data) throws {
|
||||
try data.write(to: offlineBnkPath)
|
||||
}
|
||||
|
||||
private static func writeEeveeBnkData(_ data: Data) throws {
|
||||
try data.write(to: eeveeBnkPath)
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
static func restoreFromEeveeBnk() throws {
|
||||
try writeOfflineBnkData(try eeveeBnkData)
|
||||
}
|
||||
|
||||
static func backupToEeveeBnk() throws {
|
||||
try writeEeveeBnkData(try offlineBnkData)
|
||||
}
|
||||
|
||||
static func patchOfflineBnk() throws {
|
||||
|
||||
let fileData = try offlineBnkData
|
||||
|
||||
let usernameLength = Int(fileData[8])
|
||||
let username = Data(fileData[9..<9 + usernameLength])
|
||||
|
||||
var blankData = try BundleHelper.shared.premiumBlankData()
|
||||
|
||||
blankData.insert(UInt8(usernameLength), at: 8)
|
||||
blankData.insert(contentsOf: username, at: 9)
|
||||
|
||||
try writeOfflineBnkData(blankData)
|
||||
}
|
||||
}
|
||||
@@ -13,35 +13,50 @@ class PopUpHelper {
|
||||
.shared()
|
||||
|
||||
static func showPopUp(
|
||||
delayed: Bool = false,
|
||||
message: String,
|
||||
buttonText: String
|
||||
buttonText: String,
|
||||
secondButtonText: String? = nil,
|
||||
onPrimaryClick: (() -> Void)? = nil,
|
||||
onSecondaryClick: (() -> Void)? = nil
|
||||
) {
|
||||
|
||||
if isPopUpShowing {
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.asyncAfter(deadline: delayed ? .now() + 3.0 : .now()) {
|
||||
|
||||
if isPopUpShowing {
|
||||
return
|
||||
}
|
||||
|
||||
let model = Dynamic.SPTEncorePopUpDialogModel
|
||||
.alloc(interface: SPTEncorePopUpDialogModel.self)
|
||||
.initWithTitle(
|
||||
"EeveeSpotify",
|
||||
description: message,
|
||||
image: nil,
|
||||
primaryButtonTitle: buttonText,
|
||||
secondaryButtonTitle: nil
|
||||
)
|
||||
let model = Dynamic.SPTEncorePopUpDialogModel
|
||||
.alloc(interface: SPTEncorePopUpDialogModel.self)
|
||||
.initWithTitle(
|
||||
"EeveeSpotify",
|
||||
description: message,
|
||||
image: nil,
|
||||
primaryButtonTitle: buttonText,
|
||||
secondaryButtonTitle: secondButtonText
|
||||
)
|
||||
|
||||
let dialog = Dynamic.SPTEncorePopUpDialog
|
||||
.alloc(interface: SPTEncorePopUpDialog.self)
|
||||
.`init`()
|
||||
|
||||
dialog.update(model)
|
||||
dialog.setEventHandler({ state in
|
||||
|
||||
switch (state) {
|
||||
|
||||
case .primary: onPrimaryClick?()
|
||||
case .secondary: onSecondaryClick?()
|
||||
|
||||
}
|
||||
|
||||
sharedPresenter.dismissPopupWithAnimate(true, clearQueue: false, completion: nil)
|
||||
isPopUpShowing.toggle()
|
||||
})
|
||||
|
||||
let dialog = Dynamic.SPTEncorePopUpDialog
|
||||
.alloc(interface: SPTEncorePopUpDialog.self)
|
||||
.`init`()
|
||||
|
||||
dialog.update(model)
|
||||
dialog.setEventHandler({
|
||||
sharedPresenter.dismissPopupWithAnimate(true, clearQueue: false, completion: nil)
|
||||
isPopUpShowing.toggle()
|
||||
})
|
||||
|
||||
sharedPresenter.presentPopUp(dialog)
|
||||
isPopUpShowing.toggle()
|
||||
sharedPresenter.presentPopUp(dialog)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import Orion
|
||||
|
||||
class HookedInstances {
|
||||
static var productState: SPTCoreProductState?
|
||||
}
|
||||
|
||||
class SPTCoreProductStateInstanceHook: ClassHook<NSObject> {
|
||||
|
||||
static let targetName = "SPTCoreProductState"
|
||||
|
||||
func stringForKey(_ key: String) -> String {
|
||||
|
||||
HookedInstances.productState = Dynamic.convert(
|
||||
target,
|
||||
to: SPTCoreProductState.self
|
||||
)
|
||||
return orig.stringForKey(key)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import Foundation
|
||||
|
||||
@objc protocol SPTCoreProductState {
|
||||
func stringForKey(_ key: String) -> String
|
||||
}
|
||||
@@ -3,5 +3,10 @@ import Foundation
|
||||
@objc protocol SPTEncorePopUpDialog {
|
||||
func `init`() -> SPTEncorePopUpDialog
|
||||
func update(_ popUpModel: SPTEncorePopUpDialogModel)
|
||||
func setEventHandler(_ handler: @escaping () -> Void)
|
||||
func setEventHandler(_ handler: @escaping (_ state: ClickState) -> Void)
|
||||
}
|
||||
|
||||
@objc enum ClickState: Int {
|
||||
case primary
|
||||
case secondary
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
func exitApplication() {
|
||||
|
||||
UIControl().sendAction(#selector(URLSessionTask.suspend), to: UIApplication.shared, for: nil)
|
||||
Timer.scheduledTimer(withTimeInterval: 0.2, repeats: false) { _ in
|
||||
exit(EXIT_SUCCESS)
|
||||
}
|
||||
}
|
||||
|
||||
class OfflineObserver: NSObject, NSFilePresenter {
|
||||
|
||||
var presentedItemURL: URL?
|
||||
var presentedItemOperationQueue: OperationQueue
|
||||
|
||||
override init() {
|
||||
presentedItemURL = OfflineHelper.offlineBnkPath
|
||||
presentedItemOperationQueue = .main
|
||||
}
|
||||
|
||||
func presentedItemDidChange() {
|
||||
|
||||
let productState = HookedInstances.productState!
|
||||
|
||||
if productState.stringForKey("player-license") == "premium" {
|
||||
|
||||
do {
|
||||
try OfflineHelper.backupToEeveeBnk()
|
||||
NSLog("[EeveeSpotify] Settings has changed, updated eevee.bnk")
|
||||
}
|
||||
catch {
|
||||
NSLog("[EeveeSpotify] Unable to update eevee.bnk: \(error)")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
PopUpHelper.showPopUp(
|
||||
message: "Spotify has just reloaded user data, and you've been switched to the Free plan. It's fine; simply restart the app, and the tweak will patch the data again. If this doesn't work, there might be a problem with the saved data. You can reset it and restart the app. Note: after resetting, you need to restart the app twice.",
|
||||
buttonText: "Restart App",
|
||||
secondButtonText: "Reset Data and Restart App",
|
||||
onPrimaryClick: {
|
||||
exitApplication()
|
||||
},
|
||||
onSecondaryClick: {
|
||||
try! FileManager.default.removeItem(at: OfflineHelper.persistentCachePath)
|
||||
exitApplication()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -27,52 +27,49 @@ struct EeveeSpotify: Tweak {
|
||||
|
||||
do {
|
||||
|
||||
let filePath = FileManager.default.urls(
|
||||
for: .applicationSupportDirectory, in: .userDomainMask
|
||||
)
|
||||
.first!
|
||||
.appendingPathComponent("PersistentCache")
|
||||
.appendingPathComponent("offline.bnk")
|
||||
defer {
|
||||
NSFileCoordinator.addFilePresenter(OfflineObserver())
|
||||
}
|
||||
|
||||
if !FileManager.default.fileExists(atPath: filePath.path) {
|
||||
do {
|
||||
try OfflineHelper.restoreFromEeveeBnk()
|
||||
NSLog("[EeveeSpotify] Restored from eevee.bnk")
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
|
||||
|
||||
PopUpHelper.showPopUp(
|
||||
message: "Please log in and restart the app to get Premium.",
|
||||
buttonText: "Okay!"
|
||||
)
|
||||
}
|
||||
|
||||
NSLog("[EeveeSpotify] Not activating due to nonexistent file: \(filePath)")
|
||||
return
|
||||
}
|
||||
|
||||
let fileData = try Data(contentsOf: filePath)
|
||||
catch CocoaError.fileReadNoSuchFile {
|
||||
NSLog("[EeveeSpotify] Not restoring from eevee.bnk: doesn't exist")
|
||||
}
|
||||
|
||||
let usernameLength = Int(fileData[8])
|
||||
let username = Data(fileData[9..<9 + usernameLength])
|
||||
//
|
||||
|
||||
var blankData = try BundleHelper.shared.premiumBlankData()
|
||||
do {
|
||||
try OfflineHelper.patchOfflineBnk()
|
||||
try OfflineHelper.backupToEeveeBnk()
|
||||
}
|
||||
|
||||
blankData.insert(UInt8(usernameLength), at: 8)
|
||||
blankData.insert(contentsOf: username, at: 9)
|
||||
catch CocoaError.fileReadNoSuchFile {
|
||||
|
||||
try blankData.write(to: filePath)
|
||||
NSLog("[EeveeSpotify] Successfully applied")
|
||||
NSLog("[EeveeSpotify] Not activating: offline.bnk doesn't exist")
|
||||
|
||||
PopUpHelper.showPopUp(
|
||||
delayed: true,
|
||||
message: "Please log in and restart the app to get Premium.",
|
||||
buttonText: "Okay!"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
catch {
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
|
||||
|
||||
PopUpHelper.showPopUp(
|
||||
message: "Unable to apply tweak: \(error)",
|
||||
buttonText: "OK"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
NSLog("[EeveeSpotify] Unable to apply tweak: \(error)")
|
||||
|
||||
PopUpHelper.showPopUp(
|
||||
delayed: true,
|
||||
message: "Unable to apply tweak: \(error)",
|
||||
buttonText: "OK"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user