This commit is contained in:
eevee
2024-10-09 21:38:29 +03:00
parent 657f808387
commit e76b62edbc
54 changed files with 470 additions and 171 deletions
+6 -11
View File
@@ -5,21 +5,17 @@ import SwiftUI
struct DarkPopUps: HookGroup { }
class EncoreLabelHook: ClassHook<UIView> {
typealias Group = DarkPopUps
static let targetName = "SPTEncoreLabel"
func intrinsicContentSize() -> CGSize {
if let viewController = WindowHelper.shared.viewController(for: target),
NSStringFromClass(type(of: viewController)) == "SPTEncorePopUpContainer"
{
let label = Dynamic.convert(target.subviews.first!, to: UILabel.self)
if let viewController = WindowHelper.shared.viewController(for: target) {
if NSStringFromClass(type(of: viewController)) == "SPTEncorePopUpContainer" {
let label = Dynamic.convert(target.subviews.first!, to: UILabel.self)
if !label.hasParent(matching: "Primary") {
label.textColor = .white
}
if !label.hasParent(matching: "Primary") {
label.textColor = .white
}
}
@@ -28,7 +24,6 @@ class EncoreLabelHook: ClassHook<UIView> {
}
class SPTEncorePopUpContainerHook: ClassHook<UIViewController> {
typealias Group = DarkPopUps
static let targetName = "SPTEncorePopUpContainer"
@@ -1,8 +1,7 @@
import UIKit
import Orion
class PopUpHelper {
struct PopUpHelper {
private static var isPopUpShowing = false
static let sharedPresenter = type(
@@ -19,9 +18,7 @@ class PopUpHelper {
onPrimaryClick: (() -> Void)? = nil,
onSecondaryClick: (() -> Void)? = nil
) {
DispatchQueue.main.asyncAfter(deadline: delayed ? .now() + 3.0 : .now()) {
if isPopUpShowing {
return
}
@@ -1,7 +1,6 @@
import UIKit
class URLSessionHelper {
static let shared = URLSessionHelper()
private var requestsMap: [URL:Data]
@@ -1,7 +1,6 @@
import UIKit
class WindowHelper {
struct WindowHelper {
static let shared = WindowHelper()
let window: UIWindow
@@ -17,7 +16,6 @@ class WindowHelper {
}
func findFirstViewController(_ regex: String) -> UIViewController? {
let rootView = self.rootViewController.view!
var result: UIViewController?
@@ -37,6 +35,10 @@ class WindowHelper {
searchViews(rootView)
return result
}
func dismissCurrentViewController() {
rootViewController.dismiss(animated: true)
}
func overrideUserInterfaceStyle(_ style: UIUserInterfaceStyle) {
window.overrideUserInterfaceStyle = style
@@ -185,7 +185,7 @@ private func loadLyricsForCurrentTrack() throws {
PopUpHelper.showPopUp(
delayed: false,
message: "musixmatch_unauthorized_popup".localized,
buttonText: "OK"
buttonText: "OK".uiKitLocalized
)
hasShownUnauthorizedPopUp.toggle()
@@ -197,7 +197,7 @@ private func loadLyricsForCurrentTrack() throws {
PopUpHelper.showPopUp(
delayed: false,
message: "musixmatch_restricted_popup".localized,
buttonText: "OK"
buttonText: "OK".uiKitLocalized
)
hasShownRestrictedPopUp.toggle()
@@ -1,7 +1,6 @@
import Foundation
struct GeniusLyricsRepository: LyricsRepository {
private let jsonDecoder: JSONDecoder
private let apiUrl = "https://api.genius.com"
private let session: URLSession
@@ -2,7 +2,6 @@ import Foundation
import UIKit
class MusixmatchLyricsRepository: LyricsRepository {
private let apiUrl = "https://apic.musixmatch.com"
var selectedLanguage: String
@@ -19,9 +18,7 @@ class MusixmatchLyricsRepository: LyricsRepository {
_ path: String,
query: [String: Any] = [:]
) throws -> Data {
var stringUrl = "\(apiUrl)\(path)"
var finalQuery = query
finalQuery["usertoken"] = UserDefaults.musixmatchToken
@@ -1,4 +1,5 @@
import Foundation
import UIKit
import NaturalLanguage
extension String {
@@ -10,6 +11,11 @@ extension String {
BundleHelper.shared.localizedString(self)
}
var uiKitLocalized: String {
let bundle = Bundle(for: UIApplication.self)
return bundle.localizedString(forKey: self, value: nil, table: nil)
}
func localizeWithFormat(_ arguments: CVarArg...) -> String{
String(format: self.localized, arguments: arguments)
}
-2
View File
@@ -2,9 +2,7 @@ import Orion
import UIKit
class UIOpenURLContextHook: ClassHook<UIOpenURLContext> {
func URL() -> URL {
let url = orig.URL()
if url.isOpenSpotifySafariExtension {
@@ -4,7 +4,7 @@ private func showHavePremiumPopUp() {
PopUpHelper.showPopUp(
delayed: true,
message: "have_premium_popup".localized,
buttonText: "ok".localized
buttonText: "OK".uiKitLocalized
)
}
@@ -8,7 +8,7 @@ class StreamQualitySettingsSectionHook: ClassHook<NSObject> {
func shouldResetSelection() -> Bool {
PopUpHelper.showPopUp(
message: "high_audio_quality_popup".localized,
buttonText: "ok".localized
buttonText: "OK".uiKitLocalized
)
return true
@@ -20,7 +20,7 @@ class StreamQualitySettingsSectionHook: ClassHook<NSObject> {
private func showOfflineModePopUp() {
PopUpHelper.showPopUp(
message: "playlist_downloading_popup".localized,
buttonText: "ok".localized
buttonText: "OK".uiKitLocalized
)
}
@@ -35,6 +35,16 @@ class ContentOffliningUIHelperImplementationHook: ClassHook<NSObject> {
pageIdentifier: String,
pageURI: URL
) -> String {
if pageIdentifier == "spotify:local-files" {
return orig.downloadToggledWithCurrentAvailability(
availability,
addAction: addAction,
removeAction: removeAction,
pageIdentifier: pageIdentifier,
pageURI: pageURI
)
}
showOfflineModePopUp()
return pageIdentifier
}
@@ -3,7 +3,6 @@ import SwiftUI
import UIKit
class ProfileSettingsSectionHook: ClassHook<NSObject> {
static let targetName = "ProfileSettingsSection"
func numberOfRows() -> Int {
@@ -11,9 +10,7 @@ class ProfileSettingsSectionHook: ClassHook<NSObject> {
}
func didSelectRow(_ row: Int) {
if row == 1 {
let rootSettingsController = WindowHelper.shared.findFirstViewController(
"RootSettingsViewController"
)!
@@ -62,9 +59,7 @@ class ProfileSettingsSectionHook: ClassHook<NSObject> {
}
func cellForRow(_ row: Int) -> UITableViewCell {
if row == 1 {
let settingsTableCell = Dynamic.SPTSettingsTableViewCell
.alloc(interface: SPTSettingsTableViewCell.self)
.initWithStyle(3, reuseIdentifier: "EeveeSpotify")
@@ -0,0 +1,43 @@
import Foundation
struct GitHubHelper {
private let apiUrl = "https://api.github.com"
private let decoder = JSONDecoder()
static let shared = GitHubHelper()
init() {
decoder.keyDecodingStrategy = .convertFromSnakeCase
}
private func perform(_ path: String) async throws -> Data {
let url = URL(string: "\(apiUrl)\(path)")!
let (data, _) = try await URLSession.shared.data(from: url)
return data
}
func getLatestRelease() async throws -> GitHubRelease {
let data = try await perform("/repos/whoeevee/EeveeSpotify/releases/latest")
return try decoder.decode(GitHubRelease.self, from: data)
}
func getUser(_ username: String) async throws -> GitHubUser {
let data = try await perform("/users/\(username)")
return try decoder.decode(GitHubUser.self, from: data)
}
func getContributors() async throws -> [GitHubUser] {
let data = try await perform("/repos/whoeevee/EeveeSpotify/contributors")
return try decoder.decode([GitHubUser].self, from: data)
}
func getEeveeContributorSections() async throws -> [EeveeContributorSection] {
let (data, _) = try await URLSession.shared.data(
from: URL(
string: "https://raw.githubusercontent.com/whoeevee/EeveeSpotify/swift/repo.altsource.json"
)!
)
return try decoder.decode([EeveeContributorSection].self, from: data)
}
}
@@ -0,0 +1,4 @@
struct EeveeContributor: Decodable, Equatable {
var username: String
var roles: [String]
}
@@ -0,0 +1,5 @@
struct EeveeContributorSection: Decodable, Equatable {
var title: String
var shuffled: Bool
var contributors: [EeveeContributor]
}
@@ -0,0 +1,3 @@
struct GitHubRelease: Decodable {
var tagName: String
}
@@ -1,5 +0,0 @@
import Foundation
struct GitHubReleaseInfo: Decodable {
var tag_name: String
}
@@ -0,0 +1,5 @@
struct GitHubUser: Decodable, Equatable {
var avatarUrl: String
var htmlUrl: String
var login: String
}
@@ -0,0 +1,51 @@
import SwiftUI
import UIKit
class ImageViewModel: ObservableObject {
@Published var image: UIImage?
private var imageCache: NSCache<NSString, UIImage>?
init(urlString: String?) {
loadImage(urlString: urlString)
}
private func loadImage(urlString: String?) {
guard let urlString = urlString else { return }
if let imageFromCache = getImageFromCache(from: urlString) {
self.image = imageFromCache
return
}
loadImageFromURL(urlString: urlString)
}
private func loadImageFromURL(urlString: String) {
guard let url = URL(string: urlString) else { return }
URLSession.shared.dataTask(with: url) { data, response, error in
guard error == nil else {
return
}
guard let data = data else {
return
}
DispatchQueue.main.async { [weak self] in
guard let loadedImage = UIImage(data: data) else { return }
self?.image = loadedImage
self?.setImageCache(image: loadedImage, key: urlString)
}
}.resume()
}
private func setImageCache(image: UIImage, key: String) {
imageCache?.setObject(image, forKey: key as NSString)
}
private func getImageFromCache(from key: String) -> UIImage? {
return imageCache?.object(forKey: key as NSString) as? UIImage
}
}
@@ -0,0 +1,33 @@
import SwiftUI
struct EeveeContributorView: View {
var contributor: EeveeContributor
var githubUser: GitHubUser
var body: some View {
VStack {
Link(destination: URL(string: githubUser.htmlUrl)!) {
HStack(spacing: 10) {
ImageView(urlString: githubUser.avatarUrl)
.frame(width: 48, height: 48)
.clipShape(Circle())
VStack(alignment: .leading, spacing: 0) {
Text(contributor.username)
.foregroundColor(.white)
.font(.headline)
ForEach(contributor.roles, id: \.self) { role in
Text(role)
}
.foregroundColor(.gray)
}
Spacer()
ChevronRightView()
}
}
}
}
}
@@ -0,0 +1,57 @@
import SwiftUI
struct EeveeContributorsSheetView: View {
@State private var users: [GitHubUser] = []
@State private var sections: [EeveeContributorSection] = []
var body: some View {
NavigationView {
VStack {
if users.isEmpty && sections.isEmpty {
ProgressView("Loading".uiKitLocalized)
}
else {
List {
ForEach(sections, id: \.title) { section in
Section {
ForEach(
section.shuffled
? section.contributors.shuffled()
: section.contributors,
id: \.username
) { contributor in
if let user = users.first(where: { $0.login == contributor.username }) {
EeveeContributorView(contributor: contributor, githubUser: user)
}
}
} header: {
Text(section.title)
}
}
}
}
}
.navigationTitle("contributors".localized)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
Button {
WindowHelper.shared.dismissCurrentViewController()
} label: {
Text("Done".uiKitLocalized)
.font(.headline)
}
}
.animation(.default, value: users)
.animation(.default, value: sections)
.onAppear {
Task {
users = try await GitHubHelper.shared.getContributors()
sections = try await GitHubHelper.shared.getEeveeContributorSections()
}
}
}
}
}
@@ -0,0 +1,55 @@
import SwiftUI
struct EeveeSettingsVersionView: View {
@State private var latestVersion: String?
@State private var isPresentingContributorsSheet = false
private func loadVersion() async throws {
let release = try await GitHubHelper.shared.getLatestRelease()
latestVersion = String(release.tagName.dropFirst(5)) // swiftX.X
}
private var isUpdateAvailable: Bool {
latestVersion != nil && latestVersion != EeveeSpotify.version
}
var body: some View {
Section {
if isUpdateAvailable {
Link(
"update_available".localized,
destination: URL(string: "https://github.com/whoeevee/EeveeSpotify/releases")!
)
}
} footer: {
VStack(alignment: .leading) {
Text("v\(EeveeSpotify.version)")
if latestVersion == nil {
HStack(spacing: 10) {
ProgressView()
Text("checking_for_update".localized)
}
}
else {
Button("\("contributors".localized)...") {
isPresentingContributorsSheet = true
}
.foregroundColor(.gray)
.font(.subheadline.weight(.semibold))
}
}
}
.sheet(isPresented: $isPresentingContributorsSheet) {
EeveeContributorsSheetView()
}
.animation(.default, value: latestVersion)
.onAppear {
Task {
try await loadVersion()
}
}
}
}
@@ -1,45 +0,0 @@
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")!
)
let tag = try JSONDecoder().decode(GitHubReleaseInfo.self, from: data).tag_name
latestVersion = String(tag.dropFirst(5))
}
private var isUpdateAvailable: Bool {
guard
let latest = Double(latestVersion),
let current = Double(EeveeSpotify.version)
else {
return false
}
return latest > current
}
@ViewBuilder func VersionSection() -> some View {
Section {
if isUpdateAvailable {
Link(
"update_available".localized,
destination: URL(string: "https://github.com/whoeevee/EeveeSpotify/releases")!
)
}
} footer: {
VStack(alignment: .leading) {
Text("v\(EeveeSpotify.version)")
if latestVersion.isEmpty {
HStack(spacing: 10) {
ProgressView()
Text("checking_for_update".localized)
}
}
}
}
}
}
@@ -4,8 +4,6 @@ import UIKit
struct EeveeSettingsView: View {
let navigationController: UINavigationController
@State var latestVersion = ""
@State private var hasShownCommonIssuesTip = UserDefaults.hasShownCommonIssuesTip
@State private var isClearingData = false
@@ -17,10 +15,17 @@ struct EeveeSettingsView: View {
)
navigationController.pushViewController(viewController, animated: true)
}
init(navigationController: UINavigationController) {
self.navigationController = navigationController
let spotifyAccentColor = UIColor(Color(hex: "#1ed760"))
UIView.appearance().tintColor = spotifyAccentColor
}
var body: some View {
List {
VersionSection()
EeveeSettingsVersionView()
if !hasShownCommonIssuesTip {
CommonIssuesTipView(
@@ -95,19 +100,13 @@ struct EeveeSettingsView: View {
}
}
}
.listStyle(GroupedListStyle())
.animation(.default, value: isClearingData)
.animation(.default, value: latestVersion)
.animation(.default, value: hasShownCommonIssuesTip)
.onAppear {
WindowHelper.shared.overrideUserInterfaceStyle(.dark)
Task {
try await loadVersion()
}
}
}
}
@@ -1,7 +1,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) {
@@ -21,17 +20,15 @@ extension EeveeLyricsSettingsView {
preferredStyle: .alert
)
alert.view.tintColor = UIColor(Color(hex: "#1ed760"))
alert.addTextField() { textField in
textField.placeholder = "---- Debug Info ---- [Device]: iPhone"
}
alert.addAction(UIAlertAction(title: "cancel".localized, style: .cancel) { _ in
alert.addAction(UIAlertAction(title: "Cancel".uiKitLocalized, style: .cancel) { _ in
lyricsSource = oldSource
})
alert.addAction(UIAlertAction(title: "ok".localized, style: .default) { _ in
alert.addAction(UIAlertAction(title: "OK".uiKitLocalized, style: .default) { _ in
let text = alert.textFields!.first!.text!
guard let token = getMusixmatchToken(text) else {
@@ -1,7 +1,6 @@
import SwiftUI
struct EeveeLyricsSettingsView: View {
@State var musixmatchToken = UserDefaults.musixmatchToken
@State var lyricsSource = UserDefaults.lyricsSource
@State var geniusFallback = UserDefaults.geniusFallback
@@ -63,8 +63,8 @@ struct EeveeUISettingsView: View {
.modifier(ListRowSeparatorHidden())
}
}
.listStyle(GroupedListStyle())
.listStyle(GroupedListStyle())
.animation(.default, value: lyricsColors)
}
}
@@ -0,0 +1,9 @@
import SwiftUI
struct ChevronRightView: View {
var body: some View {
Image(systemName: "chevron.right")
.font(.subheadline.weight(.semibold))
.foregroundColor(Color(UIColor.systemGray2))
}
}
@@ -1,7 +1,6 @@
import SwiftUI
struct CommonIssuesTipView: View {
var onDismiss: () -> Void
var body: some View {
@@ -18,7 +17,9 @@ struct CommonIssuesTipView: View {
}
Link(
destination: URL(string: "https://github.com/whoeevee/EeveeSpotify/blob/swift/common_issues.md")!,
destination: URL(
string: "https://github.com/whoeevee/EeveeSpotify/blob/swift/common_issues.md"
)!,
label: {
VStack {
Text("\("common_issues_tip_message".localized) ")
@@ -0,0 +1,14 @@
import SwiftUI
struct ImageView: View {
@ObservedObject private var imageViewModel: ImageViewModel
init(urlString: String?) {
imageViewModel = ImageViewModel(urlString: urlString)
}
var body: some View {
Image(uiImage: imageViewModel.image ?? UIImage())
.resizable()
}
}
@@ -1,7 +1,6 @@
import SwiftUI
struct NavigationSectionView: View {
var color: Color
var title: String
var imageSystemName: String
@@ -23,9 +22,7 @@ struct NavigationSectionView: View {
Spacer()
Image(systemName: "chevron.right")
.foregroundColor(Color(UIColor.systemGray2))
.font(.subheadline.bold())
ChevronRightView()
}
}
}
+1 -2
View File
@@ -11,8 +11,7 @@ func exitApplication() {
struct PremiumPatching: HookGroup { }
struct EeveeSpotify: Tweak {
static let version = "5.4"
static let version = "5.5"
static let isOldSpotifyVersion = NSClassFromString("Lyrics_NPVCommunicatorImpl.LyricsOnlyViewController") == nil
init() {