mirror of
https://github.com/whoeevee/EeveeSpotifyReborn.git
synced 2026-01-09 00:23:20 +01:00
v5.5
This commit is contained in:
@@ -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,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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-5
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
[
|
||||
{
|
||||
"title": "Developement",
|
||||
"shuffled": false,
|
||||
"contributors": [
|
||||
{
|
||||
"username": "whoeevee",
|
||||
"roles": [
|
||||
"Owner",
|
||||
"Main Developer"
|
||||
]
|
||||
},
|
||||
{
|
||||
"username": "asdfzxcvbn",
|
||||
"roles": [
|
||||
"Collaborator",
|
||||
"EeveeRepoUpdater"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Localization",
|
||||
"shuffled": true,
|
||||
"contributors": [
|
||||
{
|
||||
"username": "longopy",
|
||||
"roles": [
|
||||
"Spanish"
|
||||
]
|
||||
},
|
||||
{
|
||||
"username": "xiangfeidexiaohuo",
|
||||
"roles": [
|
||||
"Simplified Chinese"
|
||||
]
|
||||
},
|
||||
{
|
||||
"username": "LivioZ",
|
||||
"roles": [
|
||||
"Italian"
|
||||
]
|
||||
},
|
||||
{
|
||||
"username": "LIKVIDATOR1337",
|
||||
"roles": [
|
||||
"Ukrainian"
|
||||
]
|
||||
},
|
||||
{
|
||||
"username": "gototheskinny",
|
||||
"roles": [
|
||||
"Turkish"
|
||||
]
|
||||
},
|
||||
{
|
||||
"username": "ElliotCHEN37",
|
||||
"roles": [
|
||||
"Traditional Chinese"
|
||||
]
|
||||
},
|
||||
{
|
||||
"username": "schweppes-0x",
|
||||
"roles": [
|
||||
"Bulgarian"
|
||||
]
|
||||
},
|
||||
{
|
||||
"username": "speedyfriend433",
|
||||
"roles": [
|
||||
"Korean"
|
||||
]
|
||||
},
|
||||
{
|
||||
"username": "Richard-NDC",
|
||||
"roles": [
|
||||
"Vietnamese"
|
||||
]
|
||||
},
|
||||
{
|
||||
"username": "wlxxd",
|
||||
"roles": [
|
||||
"German"
|
||||
]
|
||||
},
|
||||
{
|
||||
"username": "Incognito-Coder",
|
||||
"roles": [
|
||||
"Persian"
|
||||
]
|
||||
},
|
||||
{
|
||||
"username": "UnexcitingDean",
|
||||
"roles": [
|
||||
"Danish"
|
||||
]
|
||||
},
|
||||
{
|
||||
"username": "An0n-00",
|
||||
"roles": [
|
||||
"Swiss German"
|
||||
]
|
||||
},
|
||||
{
|
||||
"username": "CukierDev",
|
||||
"roles": [
|
||||
"Polish"
|
||||
]
|
||||
},
|
||||
{
|
||||
"username": "3xynos7",
|
||||
"roles": [
|
||||
"Nepalese"
|
||||
]
|
||||
},
|
||||
{
|
||||
"username": "by3lish",
|
||||
"roles": [
|
||||
"Azerbaijani"
|
||||
]
|
||||
},
|
||||
{
|
||||
"username": "5jd",
|
||||
"roles": [
|
||||
"French"
|
||||
]
|
||||
},
|
||||
{
|
||||
"username": "emal0n",
|
||||
"roles": [
|
||||
"Brazil"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -1,6 +1,6 @@
|
||||
Package: com.eevee.spotify
|
||||
Name: EeveeSpotify
|
||||
Version: 5.4
|
||||
Version: 5.5
|
||||
Architecture: iphoneos-arm
|
||||
Description: A tweak to get Spotify Premium for free, just like Spotilife
|
||||
Maintainer: Eevee
|
||||
|
||||
@@ -15,9 +15,6 @@ reset_data_description = "Keş yaddaşını təmizləyin və tətbiqi yenidən b
|
||||
checking_for_update = "Yeniləmələr yoxlanılır...";
|
||||
update_available = "Yeniləmə Mövcuddur";
|
||||
|
||||
cancel = "Ləğv et";
|
||||
ok = "OK";
|
||||
|
||||
// Patching
|
||||
|
||||
do_not_patch_premium = "Premium üçün yamaqlama";
|
||||
|
||||
@@ -15,9 +15,6 @@ reset_data_description = "Изчистете кешираните данни и
|
||||
checking_for_update = "Проверка за актуализация...";
|
||||
update_available = "Налична е актуализация";
|
||||
|
||||
cancel = "Отказ";
|
||||
ok = "ОК";
|
||||
|
||||
// Patching
|
||||
|
||||
do_not_patch_premium = "Не пачвай Premium";
|
||||
|
||||
@@ -15,9 +15,6 @@ reset_data_description = "Ryd cachelagrede data og genstart appen.";
|
||||
checking_for_update = "Tjekker for opdateringer...";
|
||||
update_available = "Opdatering tilgængelig";
|
||||
|
||||
cancel = "Annuller";
|
||||
ok = "OK";
|
||||
|
||||
// Patching
|
||||
|
||||
do_not_patch_premium = "Patch ikke Premium";
|
||||
|
||||
@@ -15,9 +15,6 @@ reset_data_description = "Lösch zwischegspeicherti Date und starte d'App neu.";
|
||||
checking_for_update = "lueg nachemene Update...";
|
||||
update_available = "Es het es Update verfüegbar!";
|
||||
|
||||
cancel = "Abbräche";
|
||||
ok = "OK";
|
||||
|
||||
// Patching
|
||||
|
||||
do_not_patch_premium = "Duen Premium nid pätche";
|
||||
|
||||
@@ -15,9 +15,6 @@ reset_data_description = "Zwischengespeicherte Daten (Cache) löschen und App ne
|
||||
checking_for_update = "Suche nach Updates...";
|
||||
update_available = "Update verfügbar";
|
||||
|
||||
cancel = "Abbrechen";
|
||||
ok = "OK";
|
||||
|
||||
// Patching
|
||||
|
||||
do_not_patch_premium = "Premium nicht patchen";
|
||||
|
||||
@@ -15,9 +15,6 @@ 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";
|
||||
@@ -105,3 +102,5 @@ let_the_music_play = "Let the music play...";
|
||||
// liked songs title, should match official spotify loc
|
||||
|
||||
liked_songs = "Liked songs";
|
||||
|
||||
contributors = "Contributors";
|
||||
|
||||
@@ -15,9 +15,6 @@ reset_data_description = "Borra los datos almacenados en caché y reinicia la ap
|
||||
checking_for_update = "Comprobando Actualizaciones...";
|
||||
update_available = "Actualización Disponible";
|
||||
|
||||
cancel = "Cancelar";
|
||||
ok = "Confirmar";
|
||||
|
||||
// Patching
|
||||
|
||||
do_not_patch_premium = "No Parchear Premium";
|
||||
|
||||
@@ -15,9 +15,6 @@ reset_data_description = "پاک کردن کش و راه اندازی مجدد";
|
||||
checking_for_update = "درحال بررسی بروزرسانی...";
|
||||
update_available = "آپدیت موجود است";
|
||||
|
||||
cancel = "لغو";
|
||||
ok = "باشه";
|
||||
|
||||
// Patching
|
||||
|
||||
do_not_patch_premium = "پچ نکردن حالت ویژه";
|
||||
|
||||
@@ -15,9 +15,6 @@ reset_data_description = "Effacez les données en cache et redémarrez l'applica
|
||||
checking_for_update = "Vérification des mises à jour...";
|
||||
update_available = "Mise à jour disponible";
|
||||
|
||||
cancel = "Annuler";
|
||||
ok = "OK";
|
||||
|
||||
// Patching
|
||||
|
||||
do_not_patch_premium = "Ne pas patcher Premium";
|
||||
|
||||
@@ -15,9 +15,6 @@ reset_data_description = "Cancella i dati memorizzati nella cache e riavvia l'ap
|
||||
checking_for_update = "Verifica aggiornamenti...";
|
||||
update_available = "Aggiornamento disponibile";
|
||||
|
||||
cancel = "Annulla";
|
||||
ok = "OK";
|
||||
|
||||
// Patching
|
||||
|
||||
do_not_patch_premium = "Non patchare Premium";
|
||||
|
||||
@@ -15,9 +15,6 @@ reset_data_description = "캐시된 데이터를 지우고 앱을 다시 시작
|
||||
checking_for_update = "업데이트 확인 중...";
|
||||
update_available = "업데이트 가능";
|
||||
|
||||
cancel = "취소";
|
||||
ok = "확인";
|
||||
|
||||
// Patching
|
||||
|
||||
do_not_patch_premium = "프리미엄으로 패치 안 함";
|
||||
@@ -100,4 +97,4 @@ unknown_error = "알 수 없는 오류";
|
||||
// Instrumental Titles
|
||||
|
||||
song_is_instrumental = "이 노래는 악기전용 노래입니다.";
|
||||
let_the_music_play = "음악이 재생되게 놔두세요...";
|
||||
let_the_music_play = "음악이 재생되게 놔두세요...";
|
||||
|
||||
@@ -15,9 +15,6 @@ reset_data_description = "क्यास डाटा खाली गर्न
|
||||
checking_for_update = "अपडेटको लागि जाँच गर्दै...";
|
||||
update_available = "अपडेट उपलब्ध छ";
|
||||
|
||||
cancel = "रद्द गर्नुहोस्";
|
||||
ok = "ठीक छ";
|
||||
|
||||
// Patching
|
||||
|
||||
do_not_patch_premium = "प्रिमियम प्याच नगर्नुहोस्";
|
||||
|
||||
@@ -15,9 +15,6 @@ reset_data_description = "Wyczyść dane cache i zrestartuj aplikacje.";
|
||||
checking_for_update = "Sprawdzanie aktualizacji...";
|
||||
update_available = "Aktualizacja Dostępna";
|
||||
|
||||
cancel = "Anuluj";
|
||||
ok = "OK";
|
||||
|
||||
// Patching
|
||||
|
||||
do_not_patch_premium = "Nie używaj łatki premium";
|
||||
|
||||
@@ -15,9 +15,6 @@ reset_data_description = "Esta função limpará os Dados em cache e reiniciará
|
||||
checking_for_update = "Procurando atualizações...";
|
||||
update_available = "Atualização disponivel";
|
||||
|
||||
cancel = "Cancelar";
|
||||
ok = "Confirmar";
|
||||
|
||||
// Patching
|
||||
|
||||
do_not_patch_premium = "Não aplicar correção ao premium";
|
||||
|
||||
@@ -15,9 +15,6 @@ reset_data_description = "Очистить кэшированные данные
|
||||
checking_for_update = "Проверка наличия обновления...";
|
||||
update_available = "Доступно обновление";
|
||||
|
||||
cancel = "Отменить";
|
||||
ok = "OK";
|
||||
|
||||
// Patching
|
||||
|
||||
do_not_patch_premium = "Не патчить Premium";
|
||||
@@ -103,3 +100,5 @@ let_the_music_play = "Пусть заиграет музыка...";
|
||||
// liked songs title, should match official spotify loc
|
||||
|
||||
liked_songs = "Тебе понравилось";
|
||||
|
||||
contributors = "Участники";
|
||||
|
||||
@@ -15,9 +15,6 @@ reset_data_description = "Önbelleğe alınan verileri temizleyin ve uygulamayı
|
||||
checking_for_update = "Güncelleme Kontrol Ediliyor...";
|
||||
update_available = "Güncelleme Mevcut";
|
||||
|
||||
cancel = "İptal";
|
||||
ok = "Tamam";
|
||||
|
||||
// Patching
|
||||
|
||||
do_not_patch_premium = "Premium için olan Yamayı Yapma";
|
||||
|
||||
@@ -15,9 +15,6 @@ reset_data_description = "Очистити кешовані дані та пер
|
||||
checking_for_update = "Перевірка наявності оновлень...";
|
||||
update_available = "Доступне оновлення";
|
||||
|
||||
cancel = "Скасувати";
|
||||
ok = "ОК";
|
||||
|
||||
// Patching
|
||||
|
||||
do_not_patch_premium = "Не патчити Premium";
|
||||
|
||||
@@ -15,9 +15,6 @@ reset_data_description = "Xóa cache và khởi động lại app.";
|
||||
checking_for_update = "Đang kiểm tra bản cập nhật...";
|
||||
update_available = "Đã có bản cập nhật mới";
|
||||
|
||||
cancel = "Hủy";
|
||||
ok = "OK";
|
||||
|
||||
// Patching
|
||||
|
||||
do_not_patch_premium = "Không vá Premium";
|
||||
|
||||
@@ -15,9 +15,6 @@ reset_data_description = "清除缓存数据并重新启动应用程序。";
|
||||
checking_for_update = "正在检查更新...";
|
||||
update_available = "发现可用更新";
|
||||
|
||||
cancel = "取消";
|
||||
ok = "好的";
|
||||
|
||||
// Patching
|
||||
|
||||
do_not_patch_premium = "不启用 Premium 补丁";
|
||||
|
||||
@@ -15,9 +15,6 @@ reset_data_description = "清除快取並重新啟動Spotify.";
|
||||
checking_for_update = "正在檢查更新...";
|
||||
update_available = "有可用的更新";
|
||||
|
||||
cancel = "取消";
|
||||
ok = "確定";
|
||||
|
||||
// 修補方案
|
||||
|
||||
do_not_patch_premium = "不修補Premium";
|
||||
|
||||
Reference in New Issue
Block a user