mirror of
https://github.com/whoeevee/EeveeSpotifyReborn.git
synced 2026-01-08 23:23:20 +00:00
real
This commit is contained in:
@@ -1,8 +1,7 @@
|
||||
import Foundation
|
||||
import Orion
|
||||
|
||||
class SPTDataLoaderServiceHook: ClassHook<NSObject> {
|
||||
|
||||
class SPTDataLoaderServiceHook: ClassHook<NSObject>, SpotifySessionDelegate {
|
||||
static let targetName = "SPTDataLoaderService"
|
||||
|
||||
// orion:new
|
||||
@@ -23,48 +22,46 @@ class SPTDataLoaderServiceHook: ClassHook<NSObject> {
|
||||
return
|
||||
}
|
||||
|
||||
if error == nil && shouldModify(url) {
|
||||
|
||||
if let buffer = URLSessionHelper.shared.obtainData(for: url) {
|
||||
|
||||
if url.isLyrics {
|
||||
|
||||
do {
|
||||
orig.URLSession(
|
||||
session,
|
||||
dataTask: task,
|
||||
didReceiveData: try getLyricsForCurrentTrack(
|
||||
originalLyrics: try? Lyrics(serializedData: buffer)
|
||||
)
|
||||
)
|
||||
|
||||
orig.URLSession(session, task: task, didCompleteWithError: nil)
|
||||
}
|
||||
catch {
|
||||
orig.URLSession(session, task: task, didCompleteWithError: error)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if error == nil,
|
||||
shouldModify(url),
|
||||
let buffer = URLSessionHelper.shared.obtainData(for: url)
|
||||
{
|
||||
if url.isLyrics {
|
||||
do {
|
||||
var customizeMessage = try CustomizeMessage(serializedData: buffer)
|
||||
modifyRemoteConfiguration(&customizeMessage.response)
|
||||
|
||||
orig.URLSession(
|
||||
session,
|
||||
dataTask: task,
|
||||
didReceiveData: try customizeMessage.serializedData()
|
||||
didReceiveData: try getLyricsForCurrentTrack(
|
||||
originalLyrics: try? Lyrics(serializedData: buffer)
|
||||
)
|
||||
)
|
||||
|
||||
orig.URLSession(session, task: task, didCompleteWithError: nil)
|
||||
|
||||
NSLog("[EeveeSpotify] Modified customize data")
|
||||
return
|
||||
}
|
||||
catch {
|
||||
NSLog("[EeveeSpotify] Unable to modify customize data: \(error)")
|
||||
orig.URLSession(session, task: task, didCompleteWithError: error)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
var customizeMessage = try CustomizeMessage(serializedData: buffer)
|
||||
modifyRemoteConfiguration(&customizeMessage.response)
|
||||
|
||||
orig.URLSession(
|
||||
session,
|
||||
dataTask: task,
|
||||
didReceiveData: try customizeMessage.serializedData()
|
||||
)
|
||||
|
||||
orig.URLSession(session, task: task, didCompleteWithError: nil)
|
||||
|
||||
NSLog("[EeveeSpotify] Modified customize data")
|
||||
return
|
||||
}
|
||||
catch {
|
||||
NSLog("[EeveeSpotify] Unable to modify customize data: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,12 +73,16 @@ class SPTDataLoaderServiceHook: ClassHook<NSObject> {
|
||||
_ session: URLSession,
|
||||
dataTask task: URLSessionDataTask,
|
||||
didReceiveResponse response: HTTPURLResponse,
|
||||
completionHandler handler: Any
|
||||
completionHandler handler: @escaping (URLSession.ResponseDisposition) -> Void
|
||||
) {
|
||||
let url = response.url!
|
||||
guard
|
||||
let request = task.currentRequest,
|
||||
let url = request.url
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
if url.isLyrics, response.statusCode != 200 {
|
||||
|
||||
let okResponse = HTTPURLResponse(
|
||||
url: url,
|
||||
statusCode: 200,
|
||||
|
||||
@@ -113,7 +113,7 @@ class LyricsOnlyViewControllerHook: ClassHook<UIViewController> {
|
||||
text.append(
|
||||
Dynamic.SPTEncoreAttributedString.alloc(interface: SPTEncoreAttributedString.self)
|
||||
.initWithString(
|
||||
"\nFallback: \(description)",
|
||||
"\n\("fallback_attribute".localized): \(description)",
|
||||
typeStyle: typeStyle,
|
||||
attributes: attributes
|
||||
)
|
||||
@@ -124,7 +124,7 @@ class LyricsOnlyViewControllerHook: ClassHook<UIViewController> {
|
||||
text.append(
|
||||
Dynamic.SPTEncoreAttributedString.alloc(interface: SPTEncoreAttributedString.self)
|
||||
.initWithString(
|
||||
"\nRomanized",
|
||||
"\n\("romanized_attribute".localized)",
|
||||
typeStyle: typeStyle,
|
||||
attributes: attributes
|
||||
)
|
||||
@@ -182,10 +182,9 @@ private func loadLyricsForCurrentTrack() throws {
|
||||
case .InvalidMusixmatchToken:
|
||||
|
||||
if !hasShownUnauthorizedPopUp {
|
||||
|
||||
PopUpHelper.showPopUp(
|
||||
delayed: false,
|
||||
message: "The tweak is unable to load lyrics from Musixmatch due to Unauthorized error. Please check or update your Musixmatch token. If you use an iPad, you should get the token from the Musixmatch app for iPad.",
|
||||
message: "musixmatch_unauthorized_popup".localized,
|
||||
buttonText: "OK"
|
||||
)
|
||||
|
||||
@@ -195,10 +194,9 @@ private func loadLyricsForCurrentTrack() throws {
|
||||
case .MusixmatchRestricted:
|
||||
|
||||
if !hasShownRestrictedPopUp {
|
||||
|
||||
PopUpHelper.showPopUp(
|
||||
delayed: false,
|
||||
message: "The tweak is unable to load lyrics from Musixmatch because they are restricted. It's likely a copyright issue due to the US IP address, so you should change it if you're in the US or use a VPN.",
|
||||
message: "musixmatch_restricted_popup".localized,
|
||||
buttonText: "OK"
|
||||
)
|
||||
|
||||
|
||||
@@ -18,10 +18,10 @@ struct LyricsDto {
|
||||
if lines.isEmpty {
|
||||
lyricsData.lines = [
|
||||
LyricsLine.with {
|
||||
$0.content = "This song is instrumental."
|
||||
$0.content = "song_is_instrumental".localized
|
||||
},
|
||||
LyricsLine.with {
|
||||
$0.content = "Let the music play..."
|
||||
$0.content = "let_the_music_play".localized
|
||||
},
|
||||
LyricsLine.with {
|
||||
$0.content = ""
|
||||
|
||||
@@ -10,12 +10,12 @@ enum LyricsError: Error, CustomStringConvertible {
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .NoSuchSong: "No Song Found"
|
||||
case .MusixmatchRestricted: "Restricted"
|
||||
case .InvalidMusixmatchToken: "Unauthorized"
|
||||
case .DecodingError: "Decoding Error"
|
||||
case .NoCurrentTrack: "No Track Instance"
|
||||
case .UnknownError: "Unknown Error"
|
||||
case .NoSuchSong: "no_such_song".localized
|
||||
case .MusixmatchRestricted: "musixmatch_restricted".localized
|
||||
case .InvalidMusixmatchToken: "invalid_musixmatch_token".localized
|
||||
case .DecodingError: "decoding_error".localized
|
||||
case .NoCurrentTrack: "no_current_track".localized
|
||||
case .UnknownError: "unknown_error".localized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,8 +187,8 @@ class MusixmatchLyricsRepository: LyricsRepository {
|
||||
let subtitleTranslatedBody = subtitleTranslated["subtitle_body"] as? String,
|
||||
let subtitlesTranslated = try? JSONDecoder().decode(
|
||||
[MusixmatchSubtitle].self, from: subtitleTranslatedBody.data(using: .utf8)!
|
||||
) {
|
||||
|
||||
)
|
||||
{
|
||||
if selectedLanguage == romanizationLanguage {
|
||||
romanized = true
|
||||
|
||||
@@ -206,12 +206,7 @@ class MusixmatchLyricsRepository: LyricsRepository {
|
||||
}
|
||||
}
|
||||
|
||||
if options.musixmatchLanguage.isEmpty
|
||||
&& options.romanization
|
||||
&& selectedLanguage != romanizationLanguage {
|
||||
|
||||
selectedLanguage = romanizationLanguage
|
||||
|
||||
if options.romanization && selectedLanguage != romanizationLanguage {
|
||||
if let translations = try? getTranslations(
|
||||
query.spotifyTrackId,
|
||||
selectedLanguage: romanizationLanguage
|
||||
|
||||
@@ -2,10 +2,17 @@ import Foundation
|
||||
import NaturalLanguage
|
||||
|
||||
extension String {
|
||||
|
||||
static func ~= (lhs: String, rhs: String) -> Bool {
|
||||
lhs.firstMatch(rhs) != nil
|
||||
}
|
||||
|
||||
var localized: String {
|
||||
BundleHelper.shared.localizedString(self)
|
||||
}
|
||||
|
||||
func localizeWithFormat(_ arguments: CVarArg...) -> String{
|
||||
String(format: self.localized, arguments: arguments)
|
||||
}
|
||||
|
||||
var range: NSRange {
|
||||
NSRange(self.startIndex..., in: self)
|
||||
|
||||
@@ -88,7 +88,7 @@ extension UserDefaults {
|
||||
static var patchType: PatchType {
|
||||
get {
|
||||
if let rawValue = defaults.object(forKey: patchTypeKey) as? Int {
|
||||
return PatchType(rawValue: rawValue)!
|
||||
return PatchType(rawValue: rawValue) ?? .requests
|
||||
}
|
||||
|
||||
return .notSet
|
||||
|
||||
@@ -3,14 +3,46 @@ import Orion
|
||||
private func showHavePremiumPopUp() {
|
||||
PopUpHelper.showPopUp(
|
||||
delayed: true,
|
||||
message: "It looks like you have an active Premium subscription, so the tweak won't patch the data or restrict the use of Premium server-sided features. You can manage this in the EeveeSpotify settings.",
|
||||
buttonText: "OK"
|
||||
message: "have_premium_popup".localized,
|
||||
buttonText: "ok".localized
|
||||
)
|
||||
}
|
||||
|
||||
class SPTCoreURLSessionDataDelegateHook: ClassHook<NSObject> {
|
||||
class SpotifySessionDelegateBootstrapHook: ClassHook<NSObject>, SpotifySessionDelegate {
|
||||
static var targetName: String {
|
||||
EeveeSpotify.isOldSpotifyVersion
|
||||
? "SPTCoreURLSessionDataDelegate"
|
||||
: "SPTDataLoaderService"
|
||||
}
|
||||
|
||||
static let targetName = "SPTCoreURLSessionDataDelegate"
|
||||
func URLSession(
|
||||
_ session: URLSession,
|
||||
dataTask task: URLSessionDataTask,
|
||||
didReceiveResponse response: HTTPURLResponse,
|
||||
completionHandler handler: @escaping (URLSession.ResponseDisposition) -> Void
|
||||
) {
|
||||
orig.URLSession(session, dataTask: task, didReceiveResponse: response, completionHandler: handler)
|
||||
}
|
||||
|
||||
func URLSession(
|
||||
_ session: URLSession,
|
||||
dataTask task: URLSessionDataTask,
|
||||
didReceiveData data: Data
|
||||
) {
|
||||
guard
|
||||
let request = task.currentRequest,
|
||||
let url = request.url
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
if url.isBootstrap {
|
||||
URLSessionHelper.shared.setOrAppend(data, for: url)
|
||||
return
|
||||
}
|
||||
|
||||
orig.URLSession(session, dataTask: task, didReceiveData: data)
|
||||
}
|
||||
|
||||
func URLSession(
|
||||
_ session: URLSession,
|
||||
@@ -23,16 +55,14 @@ class SPTCoreURLSessionDataDelegateHook: ClassHook<NSObject> {
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if error == nil && url.isBootstrap {
|
||||
|
||||
let buffer = URLSessionHelper.shared.obtainData(for: url)!
|
||||
|
||||
do {
|
||||
var bootstrapMessage = try BootstrapMessage(serializedData: buffer)
|
||||
|
||||
if UserDefaults.patchType == .notSet {
|
||||
|
||||
if bootstrapMessage.attributes["type"]?.stringValue == "premium" {
|
||||
UserDefaults.patchType = .disabled
|
||||
showHavePremiumPopUp()
|
||||
@@ -46,7 +76,6 @@ class SPTCoreURLSessionDataDelegateHook: ClassHook<NSObject> {
|
||||
}
|
||||
|
||||
if UserDefaults.patchType == .requests {
|
||||
|
||||
modifyRemoteConfiguration(&bootstrapMessage.ucsResponse)
|
||||
|
||||
orig.URLSession(
|
||||
@@ -71,24 +100,4 @@ class SPTCoreURLSessionDataDelegateHook: ClassHook<NSObject> {
|
||||
|
||||
orig.URLSession(session, task: task, didCompleteWithError: error)
|
||||
}
|
||||
|
||||
func URLSession(
|
||||
_ session: URLSession,
|
||||
dataTask task: URLSessionDataTask,
|
||||
didReceiveData data: Data
|
||||
) {
|
||||
guard
|
||||
let request = task.currentRequest,
|
||||
let url = request.url
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
if url.isBootstrap {
|
||||
URLSessionHelper.shared.setOrAppend(data, for: url)
|
||||
return
|
||||
}
|
||||
|
||||
orig.URLSession(session, dataTask: task, didReceiveData: data)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import Foundation
|
||||
|
||||
func modifyRemoteConfiguration(_ configuration: inout UcsResponse) {
|
||||
|
||||
if UserDefaults.overwriteConfiguration {
|
||||
configuration.resolve.configuration = try! BundleHelper.shared.resolveConfiguration()
|
||||
}
|
||||
@@ -10,7 +9,6 @@ func modifyRemoteConfiguration(_ configuration: inout UcsResponse) {
|
||||
}
|
||||
|
||||
func modifyAttributes(_ attributes: inout [String: AccountAttribute]) {
|
||||
|
||||
attributes["type"] = AccountAttribute.with {
|
||||
$0.stringValue = "premium"
|
||||
}
|
||||
@@ -3,38 +3,32 @@ import SwiftUI
|
||||
import libroot
|
||||
|
||||
class BundleHelper {
|
||||
|
||||
private let bundleName = "EeveeSpotify"
|
||||
|
||||
|
||||
private let bundle: Bundle
|
||||
static let shared = BundleHelper()
|
||||
|
||||
private init() {
|
||||
|
||||
private init() {
|
||||
self.bundle = Bundle(
|
||||
path: Bundle.main.path(
|
||||
forResource: bundleName,
|
||||
forResource: bundleName,
|
||||
ofType: "bundle"
|
||||
)
|
||||
)
|
||||
?? jbRootPath("/Library/Application Support/\(bundleName).bundle")
|
||||
)!
|
||||
}
|
||||
|
||||
func uiImage(_ name: String) -> UIImage {
|
||||
return UIImage(
|
||||
contentsOfFile: self.bundle.path(
|
||||
forResource: name,
|
||||
ofType: "png"
|
||||
)!
|
||||
)!
|
||||
}
|
||||
|
||||
func premiumBlankData() throws -> Data {
|
||||
return try Data(
|
||||
contentsOf: self.bundle.url(
|
||||
forResource: "premiumblank",
|
||||
withExtension: "bnk"
|
||||
func uiImage(_ name: String) -> UIImage {
|
||||
return UIImage(
|
||||
contentsOfFile: self.bundle.path(
|
||||
forResource: name,
|
||||
ofType: "png"
|
||||
)!
|
||||
)
|
||||
)!
|
||||
}
|
||||
|
||||
func localizedString(_ key: String) -> String {
|
||||
return bundle.localizedString(forKey: key, value: nil, table: nil)
|
||||
}
|
||||
|
||||
func resolveConfiguration() throws -> ResolveConfiguration {
|
||||
|
||||
@@ -1,73 +1,33 @@
|
||||
import Foundation
|
||||
|
||||
class OfflineHelper {
|
||||
|
||||
static let persistentCachePath = FileManager.default.urls(
|
||||
static private let applicationSupportPath = 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)
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
static func resetPersistentCache() throws {
|
||||
static private let persistentCachePath = applicationSupportPath
|
||||
.appendingPathComponent("PersistentCache")
|
||||
|
||||
static private let remoteConfigPath = applicationSupportPath
|
||||
.appendingPathComponent("remote-config")
|
||||
|
||||
//
|
||||
|
||||
static private func resetPersistentCache() throws {
|
||||
try FileManager.default.removeItem(at: self.persistentCachePath)
|
||||
}
|
||||
|
||||
static func resetOfflineBnk() throws {
|
||||
try FileManager.default.removeItem(at: self.offlineBnkPath)
|
||||
static private func resetRemoteConfig() throws {
|
||||
try FileManager.default.removeItem(at: self.remoteConfigPath)
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
static func resetData() {
|
||||
try? resetPersistentCache()
|
||||
try? resetRemoteConfig()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,7 @@ import Foundation
|
||||
enum PatchType: Int {
|
||||
case notSet
|
||||
case disabled
|
||||
case offlineBnk
|
||||
case requests
|
||||
|
||||
var isPatching: Bool {
|
||||
self == .requests || self == .offlineBnk
|
||||
}
|
||||
var isPatching: Bool { self == .requests }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import Foundation
|
||||
|
||||
protocol SpotifySessionDelegate {
|
||||
func URLSession(
|
||||
_ session: URLSession,
|
||||
task: URLSessionDataTask,
|
||||
didCompleteWithError error: Error?
|
||||
)
|
||||
|
||||
func URLSession(
|
||||
_ session: URLSession,
|
||||
dataTask task: URLSessionDataTask,
|
||||
didReceiveData data: Data
|
||||
)
|
||||
|
||||
func URLSession(
|
||||
_ session: URLSession,
|
||||
dataTask task: URLSessionDataTask,
|
||||
didReceiveResponse response: HTTPURLResponse,
|
||||
completionHandler handler: @escaping (URLSession.ResponseDisposition) -> Void
|
||||
)
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
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("type") == "premium" {
|
||||
|
||||
// if productState.stringForKey("shuffle") == "0" {
|
||||
// return
|
||||
// }
|
||||
|
||||
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 cached data. You can reset it and restart the app. Note: after resetting, you need to restart the app twice. You can also manage the Premium patching method in the EeveeSpotify settings.",
|
||||
buttonText: "Restart App",
|
||||
secondButtonText: "Reset Data and Restart App",
|
||||
onPrimaryClick: {
|
||||
exitApplication()
|
||||
},
|
||||
onSecondaryClick: {
|
||||
try! OfflineHelper.resetPersistentCache()
|
||||
exitApplication()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -4,15 +4,13 @@ import UIKit
|
||||
struct ServerSidedReminder: HookGroup { }
|
||||
|
||||
class StreamQualitySettingsSectionHook: ClassHook<NSObject> {
|
||||
|
||||
typealias Group = ServerSidedReminder
|
||||
static let targetName = "StreamQualitySettingsSection"
|
||||
|
||||
func shouldResetSelection() -> Bool {
|
||||
|
||||
PopUpHelper.showPopUp(
|
||||
message: "Very high audio quality is server-sided and is not available with this tweak.",
|
||||
buttonText: "OK"
|
||||
message: "high_audio_quality_popup".localized,
|
||||
buttonText: "ok".localized
|
||||
)
|
||||
|
||||
return true
|
||||
@@ -23,13 +21,12 @@ class StreamQualitySettingsSectionHook: ClassHook<NSObject> {
|
||||
|
||||
private func showOfflineModePopUp() {
|
||||
PopUpHelper.showPopUp(
|
||||
message: "Native playlist downloading is server-sided and is not available with this tweak. You can download podcast episodes though.",
|
||||
buttonText: "OK"
|
||||
message: "playlist_downloading_popup".localized,
|
||||
buttonText: "ok".localized
|
||||
)
|
||||
}
|
||||
|
||||
class FTPDownloadActionHook: ClassHook<NSObject> {
|
||||
|
||||
typealias Group = ServerSidedReminder
|
||||
static let targetName = "ListUXPlatform_FreeTierPlaylistImpl.FTPDownloadAction"
|
||||
|
||||
@@ -39,7 +36,6 @@ class FTPDownloadActionHook: ClassHook<NSObject> {
|
||||
}
|
||||
|
||||
class UIButtonHook: ClassHook<UIButton> {
|
||||
|
||||
typealias Group = ServerSidedReminder
|
||||
|
||||
func setHighlighted(_ highlighted: Bool) {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import SwiftUI
|
||||
|
||||
extension EeveeSettingsView {
|
||||
|
||||
func loadVersion() async throws {
|
||||
|
||||
let (data, _) = try await URLSession.shared.data(
|
||||
from: URL(string: "https://api.github.com/repos/whoeevee/EeveeSpotify/releases/latest")!
|
||||
)
|
||||
@@ -24,11 +22,10 @@ extension EeveeSettingsView {
|
||||
}
|
||||
|
||||
@ViewBuilder func VersionSection() -> some View {
|
||||
|
||||
Section {
|
||||
if isUpdateAvailable {
|
||||
Link(
|
||||
"Update Available",
|
||||
"update_available".localized,
|
||||
destination: URL(string: "https://github.com/whoeevee/EeveeSpotify/releases")!
|
||||
)
|
||||
}
|
||||
@@ -39,7 +36,7 @@ extension EeveeSettingsView {
|
||||
if latestVersion.isEmpty {
|
||||
HStack(spacing: 10) {
|
||||
ProgressView()
|
||||
Text("Checking for Update...")
|
||||
Text("checking_for_update".localized)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import SwiftUI
|
||||
import UIKit
|
||||
|
||||
struct EeveeSettingsView: View {
|
||||
|
||||
let navigationController: UINavigationController
|
||||
|
||||
@State var latestVersion = ""
|
||||
@@ -19,7 +18,6 @@ struct EeveeSettingsView: View {
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
|
||||
VersionSection()
|
||||
|
||||
if !hasShownCommonIssuesTip {
|
||||
@@ -34,43 +32,52 @@ struct EeveeSettingsView: View {
|
||||
//
|
||||
|
||||
Button {
|
||||
pushSettingsController(with: EeveePatchingSettingsView(), title: "Patching")
|
||||
pushSettingsController(
|
||||
with: EeveePatchingSettingsView(),
|
||||
title: "patching".localized
|
||||
)
|
||||
} label: {
|
||||
NavigationSectionView(
|
||||
color: .orange,
|
||||
title: "Patching",
|
||||
title: "patching".localized,
|
||||
imageSystemName: "hammer.fill"
|
||||
)
|
||||
}
|
||||
|
||||
Button {
|
||||
pushSettingsController(with: EeveeLyricsSettingsView(), title: "Lyrics")
|
||||
pushSettingsController(
|
||||
with: EeveeLyricsSettingsView(),
|
||||
title: "lyrics".localized
|
||||
)
|
||||
} label: {
|
||||
NavigationSectionView(
|
||||
color: .blue,
|
||||
title: "Lyrics",
|
||||
title: "lyrics".localized,
|
||||
imageSystemName: "quote.bubble.fill"
|
||||
)
|
||||
}
|
||||
|
||||
Button {
|
||||
pushSettingsController(with: EeveeUISettingsView(), title: "Customization")
|
||||
pushSettingsController(
|
||||
with: EeveeUISettingsView(),
|
||||
title: "customization".localized
|
||||
)
|
||||
} label: {
|
||||
NavigationSectionView(
|
||||
color: Color(hex: "#64D2FF"),
|
||||
title: "Customization",
|
||||
title: "customization".localized,
|
||||
imageSystemName: "paintpalette.fill"
|
||||
)
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
Section(footer: Text("Clear cached data and restart the app.")) {
|
||||
Section(footer: Text("reset_data_description".localized)) {
|
||||
Button {
|
||||
try! OfflineHelper.resetPersistentCache()
|
||||
OfflineHelper.resetData()
|
||||
exitApplication()
|
||||
} label: {
|
||||
Text("Reset Data")
|
||||
Text("reset_data".localized)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import SwiftUI
|
||||
extension EeveeLyricsSettingsView {
|
||||
|
||||
func getMusixmatchToken(_ input: String) -> String? {
|
||||
|
||||
if let match = input.firstMatch("\\[UserToken\\]: ([a-f0-9]+)"),
|
||||
let tokenRange = Range(match.range(at: 1), in: input) {
|
||||
return String(input[tokenRange])
|
||||
@@ -16,10 +15,9 @@ extension EeveeLyricsSettingsView {
|
||||
}
|
||||
|
||||
func showMusixmatchTokenAlert(_ oldSource: LyricsSource) {
|
||||
|
||||
let alert = UIAlertController(
|
||||
title: "Enter User Token",
|
||||
message: "In order to use Musixmatch, you need to retrieve your user token from the official app. Download Musixmatch from the App Store, sign up, then go to Settings > Get help > Copy debug info, and paste it here. You can also extract the token using MITM.",
|
||||
title: "enter_user_token".localized,
|
||||
message: "enter_user_token_message".localized,
|
||||
preferredStyle: .alert
|
||||
)
|
||||
|
||||
@@ -29,11 +27,11 @@ extension EeveeLyricsSettingsView {
|
||||
textField.placeholder = "---- Debug Info ---- [Device]: iPhone"
|
||||
}
|
||||
|
||||
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in
|
||||
alert.addAction(UIAlertAction(title: "cancel".localized, style: .cancel) { _ in
|
||||
lyricsSource = oldSource
|
||||
})
|
||||
|
||||
alert.addAction(UIAlertAction(title: "OK", style: .default) { _ in
|
||||
alert.addAction(UIAlertAction(title: "ok".localized, style: .default) { _ in
|
||||
let text = alert.textFields!.first!.text!
|
||||
|
||||
guard let token = getMusixmatchToken(text) else {
|
||||
|
||||
@@ -3,26 +3,15 @@ import SwiftUI
|
||||
extension EeveeLyricsSettingsView {
|
||||
|
||||
func lyricsSourceFooter() -> some View {
|
||||
var text = """
|
||||
You can select the lyrics source you prefer.
|
||||
|
||||
Genius: Offers the best quality lyrics, provides the most songs, and updates lyrics the fastest. Does not and will never be time-synced.
|
||||
|
||||
LRCLIB: The most open service, offering time-synced lyrics. However, it lacks lyrics for many songs.
|
||||
|
||||
Musixmatch: The service Spotify uses. Provides time-synced lyrics for many songs, but you'll need a user token to use this source.
|
||||
"""
|
||||
var text = "lyrics_source_description".localized
|
||||
|
||||
if Locale.isInRegion("JP", orHasLanguage: "ja") {
|
||||
text.append("\n\n")
|
||||
text.append("PetitLyrics: Offers plenty of time-synced Japanese and some international lyrics.")
|
||||
text.append("petitlyrics_description".localized)
|
||||
}
|
||||
|
||||
text.append("\n\n")
|
||||
|
||||
text.append("""
|
||||
If the tweak is unable to find a song or process the lyrics, you'll see a "Couldn't load the lyrics for this song" message. The lyrics might be wrong for some songs when using Genius due to how the tweak searches songs. I've made it work in most cases.
|
||||
""")
|
||||
text.append("lyrics_additional_info".localized)
|
||||
|
||||
return Text(text)
|
||||
}
|
||||
@@ -30,7 +19,7 @@ If the tweak is unable to find a song or process the lyrics, you'll see a "Could
|
||||
@ViewBuilder func LyricsSourceSection() -> some View {
|
||||
Section(footer: lyricsSourceFooter()) {
|
||||
Picker(
|
||||
"Lyrics Source",
|
||||
"lyrics_source".localized,
|
||||
selection: $lyricsSource
|
||||
) {
|
||||
Text("Genius").tag(LyricsSource.genius)
|
||||
@@ -45,9 +34,9 @@ If the tweak is unable to find a song or process the lyrics, you'll see a "Could
|
||||
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
|
||||
Text("Musixmatch User Token")
|
||||
Text("musixmatch_user_token".localized)
|
||||
|
||||
TextField("Enter User Token or Paste Debug Info", text: $musixmatchToken)
|
||||
TextField("user_token_placeholder".localized, text: $musixmatchToken)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
@@ -55,8 +44,9 @@ If the tweak is unable to find a song or process the lyrics, you'll see a "Could
|
||||
}
|
||||
|
||||
.onChange(of: musixmatchToken) { input in
|
||||
|
||||
if input.isEmpty { return }
|
||||
if input.isEmpty {
|
||||
return
|
||||
}
|
||||
|
||||
if let token = getMusixmatchToken(input) {
|
||||
UserDefaults.musixmatchToken = token
|
||||
@@ -68,7 +58,6 @@ If the tweak is unable to find a song or process the lyrics, you'll see a "Could
|
||||
}
|
||||
|
||||
.onChange(of: lyricsSource) { [lyricsSource] newSource in
|
||||
|
||||
if newSource == .musixmatch && musixmatchToken.isEmpty {
|
||||
showMusixmatchTokenAlert(lyricsSource)
|
||||
return
|
||||
|
||||
@@ -11,21 +11,20 @@ struct EeveeLyricsSettingsView: View {
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
|
||||
LyricsSourceSection()
|
||||
|
||||
if lyricsSource != .genius {
|
||||
Section(
|
||||
footer: Text("Load lyrics from Genius if there is a problem with \(lyricsSource).")
|
||||
footer: Text("genius_fallback_description".localizeWithFormat(lyricsSource.description))
|
||||
) {
|
||||
Toggle(
|
||||
"Genius Fallback",
|
||||
"genius_fallback".localized,
|
||||
isOn: $geniusFallback
|
||||
)
|
||||
|
||||
if geniusFallback {
|
||||
Toggle(
|
||||
"Show Fallback Reasons",
|
||||
"show_fallback_reasons".localized,
|
||||
isOn: Binding<Bool>(
|
||||
get: { UserDefaults.fallbackReasons },
|
||||
set: { UserDefaults.fallbackReasons = $0 }
|
||||
@@ -37,9 +36,9 @@ struct EeveeLyricsSettingsView: View {
|
||||
|
||||
//
|
||||
|
||||
Section(footer: Text("Display romanized lyrics for Japanese, Korean, and Chinese.")) {
|
||||
Section(footer: Text("romanized_lyrics_description".localized)) {
|
||||
Toggle(
|
||||
"Romanized Lyrics",
|
||||
"romanized_lyrics".localized,
|
||||
isOn: $lyricsOptions.romanization
|
||||
)
|
||||
}
|
||||
@@ -53,7 +52,7 @@ struct EeveeLyricsSettingsView: View {
|
||||
.foregroundColor(.yellow)
|
||||
}
|
||||
|
||||
Text("Musixmatch Lyrics Language")
|
||||
Text("musixmatch_language".localized)
|
||||
|
||||
Spacer()
|
||||
|
||||
@@ -62,7 +61,7 @@ struct EeveeLyricsSettingsView: View {
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
} footer: {
|
||||
Text("You can enter a 2-letter Musixmatch language code and see translated lyrics on Musixmatch if they are available.")
|
||||
Text("musixmatch_language_description".localized)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +84,6 @@ struct EeveeLyricsSettingsView: View {
|
||||
}
|
||||
|
||||
.onChange(of: lyricsOptions) { lyricsOptions in
|
||||
|
||||
let selectedLanguage = lyricsOptions.musixmatchLanguage
|
||||
|
||||
if selectedLanguage.isEmpty || selectedLanguage ~= "^[\\w\\d]{2}$" {
|
||||
|
||||
@@ -7,64 +7,32 @@ struct EeveePatchingSettingsView: View {
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section(footer: patchType == .disabled ? nil : Text("""
|
||||
You can select the Premium patching method you prefer. App restart is required after changing.
|
||||
|
||||
Static: The original method. On app start, the tweak composes cache data by inserting your username into a blank file with preset Premium parameters. When Spotify reloads user data, you'll be switched to the Free plan and see a popup with quick restart app and reset data actions.
|
||||
|
||||
Dynamic: This method intercepts requests to load user data, deserializes it, and modifies the parameters in real-time. It's much more stable and is recommended.
|
||||
|
||||
If you have an active Premium subscription, you can turn on Do Not Patch Premium. The tweak won't patch the data or restrict the use of Premium server-sided features.
|
||||
""")) {
|
||||
Section(footer: patchType == .disabled ? nil : Text("patching_description".localized)) {
|
||||
Toggle(
|
||||
"Do Not Patch Premium",
|
||||
"do_not_patch_premium".localized,
|
||||
isOn: Binding<Bool>(
|
||||
get: { patchType == .disabled },
|
||||
set: { patchType = $0 ? .disabled : .requests }
|
||||
)
|
||||
)
|
||||
|
||||
if patchType != .disabled {
|
||||
Picker(
|
||||
"Patching Method",
|
||||
selection: $patchType
|
||||
) {
|
||||
Text("Static").tag(PatchType.offlineBnk)
|
||||
Text("Dynamic").tag(PatchType.requests)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.onChange(of: patchType) { newPatchType in
|
||||
|
||||
UserDefaults.patchType = newPatchType
|
||||
|
||||
do {
|
||||
try OfflineHelper.resetOfflineBnk()
|
||||
}
|
||||
catch {
|
||||
NSLog("Unable to reset offline.bnk: \(error)")
|
||||
}
|
||||
OfflineHelper.resetData()
|
||||
}
|
||||
|
||||
.onChange(of: overwriteConfiguration) { overwriteConfiguration in
|
||||
|
||||
UserDefaults.overwriteConfiguration = overwriteConfiguration
|
||||
|
||||
do {
|
||||
try OfflineHelper.resetOfflineBnk()
|
||||
}
|
||||
catch {
|
||||
NSLog("Unable to reset offline.bnk: \(error)")
|
||||
}
|
||||
OfflineHelper.resetData()
|
||||
}
|
||||
|
||||
if patchType == .requests {
|
||||
Section(
|
||||
footer: Text("Replace remote configuration with the dumped Premium one. It might fix some issues, such as appearing ads, but it's not guaranteed.")
|
||||
footer: Text("overwrite_configuration_description".localized)
|
||||
) {
|
||||
Toggle(
|
||||
"Overwrite Configuration",
|
||||
"overwrite_configuration".localized,
|
||||
isOn: $overwriteConfiguration
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,31 +2,26 @@ import SwiftUI
|
||||
import UIKit
|
||||
|
||||
struct EeveeUISettingsView: View {
|
||||
|
||||
@State var lyricsColors = UserDefaults.lyricsColors
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section(
|
||||
header: Text("Lyrics Background Color"),
|
||||
footer: Text("""
|
||||
If you turn on Display Original Colors, the lyrics will appear in the original Spotify colors for tracks that have them.
|
||||
|
||||
You can set a static color or a normalization factor based on the extracted track cover's color. This factor determines how much dark colors are lightened and light colors are darkened. Generally, you will see lighter colors with a higher normalization factor.
|
||||
""")) {
|
||||
header: Text("lyrics_background_color_section".localized),
|
||||
footer: Text("lyrics_background_color_section_description".localized)) {
|
||||
Toggle(
|
||||
"Display Original Colors",
|
||||
"display_original_colors".localized,
|
||||
isOn: $lyricsColors.displayOriginalColors
|
||||
)
|
||||
|
||||
Toggle(
|
||||
"Use Static Color",
|
||||
"use_static_color".localized,
|
||||
isOn: $lyricsColors.useStaticColor
|
||||
)
|
||||
|
||||
if lyricsColors.useStaticColor {
|
||||
ColorPicker(
|
||||
"Static Color",
|
||||
"static_color".localized,
|
||||
selection: Binding<Color>(
|
||||
get: { Color(hex: lyricsColors.staticColor) },
|
||||
set: { lyricsColors.staticColor = $0.hexString }
|
||||
@@ -36,8 +31,7 @@ struct EeveeUISettingsView: View {
|
||||
}
|
||||
else {
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
|
||||
Text("Color Normalization Factor")
|
||||
Text("color_normalization_factor".localized)
|
||||
|
||||
Slider(
|
||||
value: $lyricsColors.normalizationFactor,
|
||||
@@ -54,7 +48,7 @@ struct EeveeUISettingsView: View {
|
||||
|
||||
Section {
|
||||
Toggle(
|
||||
"Dark PopUps",
|
||||
"dark_popups".localized,
|
||||
isOn: Binding<Bool>(
|
||||
get: { UserDefaults.darkPopUps },
|
||||
set: { UserDefaults.darkPopUps = $0 }
|
||||
|
||||
@@ -2,7 +2,6 @@ import Orion
|
||||
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)
|
||||
@@ -15,78 +14,12 @@ struct EeveeSpotify: Tweak {
|
||||
static let isOldSpotifyVersion = NSClassFromString("Lyrics_NPVCommunicatorImpl.LyricsOnlyViewController") == nil
|
||||
|
||||
init() {
|
||||
|
||||
do {
|
||||
|
||||
defer {
|
||||
|
||||
if UserDefaults.darkPopUps {
|
||||
DarkPopUps().activate()
|
||||
}
|
||||
|
||||
let patchType = UserDefaults.patchType
|
||||
|
||||
if patchType.isPatching {
|
||||
|
||||
if patchType == .offlineBnk {
|
||||
NSFileCoordinator.addFilePresenter(OfflineObserver())
|
||||
}
|
||||
|
||||
ServerSidedReminder().activate()
|
||||
}
|
||||
}
|
||||
|
||||
switch UserDefaults.patchType {
|
||||
|
||||
case .disabled:
|
||||
NSLog("[EeveeSpotify] Not activating: patchType is disabled")
|
||||
|
||||
case .offlineBnk:
|
||||
do {
|
||||
try OfflineHelper.restoreFromEeveeBnk()
|
||||
|
||||
NSLog("[EeveeSpotify] Restored from eevee.bnk")
|
||||
return
|
||||
}
|
||||
|
||||
catch CocoaError.fileReadNoSuchFile {
|
||||
NSLog("[EeveeSpotify] Not restoring from eevee.bnk: doesn't exist")
|
||||
}
|
||||
|
||||
do {
|
||||
try OfflineHelper.patchOfflineBnk()
|
||||
try OfflineHelper.backupToEeveeBnk()
|
||||
}
|
||||
|
||||
catch CocoaError.fileReadNoSuchFile {
|
||||
|
||||
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: "OK"
|
||||
)
|
||||
}
|
||||
|
||||
case .notSet:
|
||||
NSLog("[EeveeSpotify] Patching method not set, resetting offline.bnk")
|
||||
try? OfflineHelper.resetOfflineBnk()
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
if UserDefaults.darkPopUps {
|
||||
DarkPopUps().activate()
|
||||
}
|
||||
|
||||
catch {
|
||||
|
||||
NSLog("[EeveeSpotify] Unable to apply tweak: \(error)")
|
||||
|
||||
PopUpHelper.showPopUp(
|
||||
delayed: true,
|
||||
message: "Unable to apply tweak: \(error)",
|
||||
buttonText: "OK"
|
||||
)
|
||||
|
||||
if UserDefaults.patchType.isPatching {
|
||||
ServerSidedReminder().activate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>English</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,98 @@
|
||||
/* MARK: Settings */
|
||||
|
||||
patching = "Patching";
|
||||
lyrics = "Lyrics";
|
||||
customization = "Customization";
|
||||
|
||||
reset_data = "Reset Data";
|
||||
reset_data_description = "Clear cached data and restart the app.";
|
||||
|
||||
checking_for_update = "Checking for Update...";
|
||||
update_available = "Update Available";
|
||||
|
||||
cancel = "Cancel";
|
||||
ok = "OK";
|
||||
|
||||
// Patching
|
||||
|
||||
do_not_patch_premium = "Do Not Patch Premium";
|
||||
patching_description = "The tweak intercepts requests to load user data, deserializes it, and modifies the parameters in real-time.
|
||||
|
||||
If you have an active Premium subscription, you can turn on Do Not Patch Premium. The tweak won't patch the data or restrict the use of Premium server-sided features. App restart is required after changing.";
|
||||
|
||||
overwrite_configuration = "Overwrite Configuration";
|
||||
overwrite_configuration_description = "Replace remote configuration with the dumped Premium one. It might fix some issues, such as appearing ads, but it's not guaranteed.";
|
||||
|
||||
// Lyrics
|
||||
|
||||
lyrics_source = "Lyrics Source";
|
||||
lyrics_source_description = "You can select the lyrics source you prefer.
|
||||
|
||||
Genius: Offers the best quality lyrics, provides the most songs, and updates lyrics the fastest. Does not and will never be time-synced.
|
||||
|
||||
LRCLIB: The most open service, offering time-synced lyrics. However, it lacks lyrics for many songs.
|
||||
|
||||
Musixmatch: The service Spotify uses. Provides time-synced lyrics for many songs, but you'll need a user token to use this source.";
|
||||
lyrics_additional_info = "If the tweak is unable to find a song or process the lyrics, you'll see a \"Couldn't load the lyrics for this song\" message. The lyrics might be wrong for some songs when using Genius due to how the tweak searches songs. I've made it work in most cases.";
|
||||
petitlyrics_description = "PetitLyrics: Offers plenty of time-synced Japanese and some international lyrics.";
|
||||
|
||||
musixmatch_user_token = "Musixmatch User Token";
|
||||
user_token_placeholder = "Enter User Token or Paste Debug Info";
|
||||
|
||||
enter_user_token = "Enter User Token";
|
||||
enter_user_token_message = "In order to use Musixmatch, you need to retrieve your user token from the official app. Download Musixmatch from the App Store, sign up, then go to Settings > Get help > Copy debug info, and paste it here. You can also extract the token using MITM.";
|
||||
|
||||
genius_fallback = "Genius Fallback";
|
||||
genius_fallback_description = "Load lyrics from Genius if there is a problem with %@.";
|
||||
|
||||
show_fallback_reasons = "Show Fallback Reasons";
|
||||
|
||||
romanized_lyrics = "Romanized Lyrics";
|
||||
romanized_lyrics_description = "Display romanized lyrics for Japanese, Korean, and Chinese.";
|
||||
|
||||
musixmatch_language = "Musixmatch Lyrics Language";
|
||||
musixmatch_language_description = "You can enter a 2-letter Musixmatch language code and see translated lyrics on Musixmatch if they are available.";
|
||||
|
||||
// UI
|
||||
|
||||
lyrics_background_color_section = "Lyrics Background Color";
|
||||
lyrics_background_color_section_description = "If you turn on Display Original Colors, the lyrics will appear in the original Spotify colors for tracks that have them.
|
||||
|
||||
You can set a static color or a normalization factor based on the extracted track cover's color. This factor determines how much dark colors are lightened and light colors are darkened. Generally, you will see lighter colors with a higher normalization factor.";
|
||||
|
||||
display_original_colors = "Display Original Colors";
|
||||
|
||||
use_static_color = "Use Static Color";
|
||||
static_color = "Static Color";
|
||||
|
||||
color_normalization_factor = "Color Normalization Factor";
|
||||
dark_popups = "Dark PopUps";
|
||||
|
||||
/* MARK: Premium PopUps */
|
||||
|
||||
have_premium_popup = "It looks like you have an active Premium subscription, so the tweak won't patch the data or restrict the use of Premium server-sided features. You can manage this in the EeveeSpotify settings.";
|
||||
|
||||
high_audio_quality_popup = "Very high audio quality is server-sided and is not available with this tweak.";
|
||||
playlist_downloading_popup = "Native playlist downloading is server-sided and is not available with this tweak. You can download podcast episodes though.";
|
||||
|
||||
/* MARK: Lyrics */
|
||||
|
||||
fallback_attribute = "Fallback";
|
||||
romanized_attribute = "Romanized";
|
||||
|
||||
musixmatch_unauthorized_popup = "The tweak is unable to load lyrics from Musixmatch due to Unauthorized error. Please check or update your Musixmatch token. If you use an iPad, you should get the token from the Musixmatch app for iPad.";
|
||||
musixmatch_restricted_popup = "The tweak is unable to load lyrics from Musixmatch because they are restricted. It's likely a copyright issue due to the US IP address, so you should change it if you're in the US or use a VPN.";
|
||||
|
||||
// Errors Titles
|
||||
|
||||
no_such_song = "No Song Found";
|
||||
musixmatch_restricted = "Restricted";
|
||||
invalid_musixmatch_token = "Unauthorized";
|
||||
decoding_error = "Decoding Error";
|
||||
no_current_track = "No Track Instance";
|
||||
unknown_error = "Unknown Error";
|
||||
|
||||
// Instrumental Titles
|
||||
|
||||
song_is_instrumental = "This song is instrumental.";
|
||||
let_the_music_play = "Let the music play...";
|
||||
Binary file not shown.
Reference in New Issue
Block a user