lrclib api option, many improvements and refactoring

This commit is contained in:
eevee
2025-03-12 21:54:10 +03:00
parent 8c443c8a9b
commit 08037dbb61
44 changed files with 505 additions and 332 deletions
@@ -96,8 +96,10 @@ class LyricsOnlyViewControllerHook: ClassHook<UIViewController> {
//
if UserDefaults.fallbackReasons, let description = lastLyricsState.fallbackError?.description {
var attributedString = Dynamic.SPTEncoreAttributedString.alloc(
if UserDefaults.lyricsOptions.showFallbackReasons,
let description = lastLyricsState.fallbackError?.description
{
let attributedString = Dynamic.SPTEncoreAttributedString.alloc(
interface: SPTEncoreAttributedString.self
)
@@ -117,7 +119,7 @@ class LyricsOnlyViewControllerHook: ClassHook<UIViewController> {
}
if lastLyricsState.wasRomanized {
var attributedString = Dynamic.SPTEncoreAttributedString.alloc(
let attributedString = Dynamic.SPTEncoreAttributedString.alloc(
interface: SPTEncoreAttributedString.self
)
@@ -146,6 +148,11 @@ class LyricsOnlyViewControllerHook: ClassHook<UIViewController> {
//
private let geniusLyricsRepository = GeniusLyricsRepository()
private let petitLyricsRepository = PetitLyricsRepository()
//
private func loadLyricsForCurrentTrack() throws {
guard let track = HookedInstances.currentTrack else {
throw LyricsError.noCurrentTrack
@@ -163,10 +170,10 @@ private func loadLyricsForCurrentTrack() throws {
var source = UserDefaults.lyricsSource
var repository: LyricsRepository = switch source {
case .genius: GeniusLyricsRepository()
case .lrclib: LrcLibLyricsRepository()
case .genius: geniusLyricsRepository
case .lrclib: LrclibLyricsRepository.shared
case .musixmatch: MusixmatchLyricsRepository.shared
case .petit: PetitLyricsRepository()
case .petit: petitLyricsRepository
case .notReplaced: throw LyricsError.invalidSource
}
@@ -215,7 +222,7 @@ private func loadLyricsForCurrentTrack() throws {
lastLyricsState.fallbackError = .unknownError
}
if source == .genius || !UserDefaults.geniusFallback {
if source == .genius || !UserDefaults.lyricsOptions.geniusFallback {
throw error
}
@@ -1,6 +1,9 @@
import Foundation
struct LyricsOptions: Codable, Equatable {
struct LyricsOptions: Codable, Hashable {
var romanization: Bool
var musixmatchLanguage: String
var lrclibUrl: String
var geniusFallback: Bool
var showFallbackReasons: Bool
}
@@ -21,7 +21,7 @@ enum LyricsSource: Int, CaseIterable, CustomStringConvertible {
}
}
var isReplacing: Bool { self != .notReplaced }
var isReplacingLyrics: Bool { self != .notReplaced }
static var defaultSource: LyricsSource {
Locale.isInRegion("JP", orHasLanguage: "ja")
@@ -1,6 +1,6 @@
import Foundation
struct GeniusLyricsRepository: LyricsRepository {
class GeniusLyricsRepository: LyricsRepository {
private let jsonDecoder: JSONDecoder
private let apiUrl = "https://api.genius.com"
private let session: URLSession
@@ -1,10 +1,12 @@
import Foundation
struct LrcLibLyricsRepository: LyricsRepository {
private let apiUrl = "https://lrclib.net/api"
class LrclibLyricsRepository: LyricsRepository {
var apiUrl: String
private let session: URLSession
init() {
private init(apiUrl: String) {
self.apiUrl = apiUrl
let configuration = URLSessionConfiguration.default
configuration.httpAdditionalHeaders = [
"User-Agent": "EeveeSpotify v\(EeveeSpotify.version) https://github.com/whoeevee/EeveeSpotify"
@@ -13,6 +15,12 @@ struct LrcLibLyricsRepository: LyricsRepository {
session = URLSession(configuration: configuration)
}
static let originalApiUrl = "https://lrclib.net/api"
static let shared = LrclibLyricsRepository(
apiUrl: UserDefaults.lyricsOptions.lrclibUrl
)
private func perform(
_ path: String,
query: [String:Any] = [:]
@@ -65,7 +73,6 @@ struct LrcLibLyricsRepository: LyricsRepository {
var captures: [String: String] = [:]
for name in ["minute", "seconds", "content"] {
let matchRange = match.range(withName: name)
if let substringRange = Range(matchRange, in: line) {
@@ -1,6 +1,6 @@
import Foundation
struct PetitLyricsRepository: LyricsRepository {
class PetitLyricsRepository: LyricsRepository {
private let url = "https://p1.petitlyrics.com/api/GetPetitLyricsData.php"
private let session: URLSession
@@ -16,7 +16,7 @@ extension String {
return bundle.localizedString(forKey: self, value: nil, table: nil)
}
func localizeWithFormat(_ arguments: CVarArg...) -> String{
func localizeWithFormat(_ arguments: CVarArg...) -> String {
String(format: self.localized, arguments: arguments)
}
@@ -5,8 +5,6 @@ extension UserDefaults {
private static let lyricsSourceKey = "lyricsSource"
private static let musixmatchTokenKey = "musixmatchToken"
private static let geniusFallbackKey = "geniusFallback"
private static let fallbackReasonsKey = "fallbackReasons"
private static let darkPopUpsKey = "darkPopUps"
private static let patchTypeKey = "patchType"
private static let overwriteConfigurationKey = "overwriteConfiguration"
@@ -36,15 +34,6 @@ extension UserDefaults {
}
}
static var geniusFallback: Bool {
get {
defaults.object(forKey: geniusFallbackKey) as? Bool ?? true
}
set (fallback) {
defaults.set(fallback, forKey: geniusFallbackKey)
}
}
static var lyricsOptions: LyricsOptions {
get {
if let data = defaults.object(forKey: lyricsOptionsKey) as? Data,
@@ -54,22 +43,16 @@ extension UserDefaults {
return LyricsOptions(
romanization: false,
musixmatchLanguage: Locale.current.languageCode ?? ""
musixmatchLanguage: Locale.current.languageCode ?? "",
lrclibUrl: LrclibLyricsRepository.originalApiUrl,
geniusFallback: true,
showFallbackReasons: true
)
}
set (lyricsOptions) {
defaults.set(try! JSONEncoder().encode(lyricsOptions), forKey: lyricsOptionsKey)
}
}
static var fallbackReasons: Bool {
get {
defaults.object(forKey: fallbackReasonsKey) as? Bool ?? true
}
set (reasons) {
defaults.set(reasons, forKey: fallbackReasonsKey)
}
}
static var darkPopUps: Bool {
get {
@@ -3,4 +3,5 @@ import Foundation
// it's NSURL actually, just with Spotify extensions
@objc protocol SPTURL {
func spt_trackIdentifier() -> String
func isPlaylistURL() -> Bool
}
@@ -26,11 +26,9 @@ class ContentOffliningUIHelperImplementationHook: ClassHook<NSObject> {
pageIdentifier: NSString,
pageURI: NSURL
) {
let isPlaylist = [
"free-tier-playlist",
"playlist/ondemand"
].contains(pageIdentifier)
let isPlaylist = Dynamic.convert(pageURI, to: SPTURL.self)
.isPlaylistURL()
PopUpHelper.showPopUp(
message: "playlist_downloading_popup".localized,
buttonText: "OK".uiKitLocalized,
@@ -1,22 +0,0 @@
import Foundation
import UIKit
struct AnonymousTokenHelper {
private static let apiUrl = "https://apic.musixmatch.com"
static func requestAnonymousMusixmatchToken() async throws -> String {
let url = URL(string: "\(apiUrl)/ws/1.1/token.get?app_id=\(await UIDevice.current.musixmatchAppId)")!
let (data, _) = try await URLSession.shared.data(from: url)
guard
let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
let message = json["message"] as? [String: Any],
let body = message["body"] as? [String: Any],
let userToken = body["user_token"] as? String
else {
throw AnonymousTokenError.invalidResponse
}
return userToken
}
}
@@ -0,0 +1,25 @@
import UIKit
import Combine
struct AnonymousTokenHelper {
private static let apiUrl = "https://apic.musixmatch.com"
static func requestAnonymousMusixmatchToken() -> AnyPublisher<String, Error> {
let url = URL(string: "\(apiUrl)/ws/1.1/token.get?app_id=\(UIDevice.current.musixmatchAppId)")!
return URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.tryMap { data in
guard let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
let message = json["message"] as? [String: Any],
let body = message["body"] as? [String: Any],
let userToken = body["user_token"] as? String
else {
throw AnonymousTokenError.invalidResponse
}
return userToken
}
.eraseToAnyPublisher()
}
}
@@ -0,0 +1,7 @@
enum LrclibURLState {
case `default`
case invalidURL
case unreachableURL
case originalURL
case ok
}
@@ -0,0 +1,81 @@
import SwiftUI
import Combine
extension EeveeLyricsSettingsViewModel {
func setupBindings() {
$lyricsOptions
.map(\.musixmatchLanguage)
.sink { [weak self] language in
guard let self = self else { return }
let isValidLanguage = language.isEmpty || language ~= "^[\\w\\d]{2}$"
if isValidLanguage {
self.showMusixmatchInvalidLanguageWarning = false
MusixmatchLyricsRepository.shared.selectedLanguage = language
return
}
self.showMusixmatchInvalidLanguageWarning = true
}
.store(in: &cancellables)
$lyricsOptions
.map(\.lrclibUrl)
.map { urlString -> AnyPublisher<LrclibURLState, Never> in
guard let url = URL(string: urlString) else {
return Just(.invalidURL).eraseToAnyPublisher()
}
if url.host == "lrclib.net" {
return Just(.originalURL).eraseToAnyPublisher()
}
return URLSession.shared.dataTaskPublisher(for: url)
.map { _ in
LrclibLyricsRepository.shared.apiUrl = urlString
return LrclibURLState.ok
}
.catch { _ in Just(LrclibURLState.unreachableURL) }
.eraseToAnyPublisher()
}
.switchToLatest()
.receive(on: DispatchQueue.main)
.assign(to: &$lrclibURLState)
$musixmatchToken
.dropFirst()
.receive(on: DispatchQueue.main)
.sink { [weak self] tokenString in
guard let self = self else { return }
if let token = self.getMusixmatchTokenFromDebugInfo(tokenString) {
self.musixmatchToken = token
return
}
if let token = self.getMusixmatchToken(tokenString) {
UserDefaults.musixmatchToken = token
}
}
.store(in: &cancellables)
$lyricsSource
.dropFirst()
.sink { [weak self] newSource in
guard let self = self else { return }
if newSource == .musixmatch && self.musixmatchToken.isEmpty {
self.musixmatchTokenInputAlertPublisher.send(true)
return
}
if newSource == .lrclib {
self.lyricsOptions.lrclibUrl = LrclibLyricsRepository.originalApiUrl
}
UserDefaults.lyricsSource = newSource
}
.store(in: &cancellables)
}
}
@@ -0,0 +1,72 @@
import SwiftUI
import Combine
class EeveeLyricsSettingsViewModel: ObservableObject {
@Published var lyricsSource = UserDefaults.lyricsSource
@Published var lyricsOptions = UserDefaults.lyricsOptions {
didSet { UserDefaults.lyricsOptions = lyricsOptions }
}
@Published var musixmatchToken = UserDefaults.musixmatchToken
@Published var isRequestingMusixmatchToken = false
@Published var musixmatchTokenInputAlertPublisher = PassthroughSubject<Bool, Never>()
var isMusixmatchTokenValid: Bool { getMusixmatchToken(musixmatchToken) != nil }
@Published var showMusixmatchInvalidLanguageWarning = false
@Published var lrclibURLState = LrclibURLState.default
var animationValues: [AnyHashable] {
[
lyricsSource,
lyricsOptions,
isMusixmatchTokenValid,
isRequestingMusixmatchToken,
lrclibURLState,
showMusixmatchInvalidLanguageWarning
]
}
var cancellables = Set<AnyCancellable>()
init() {
setupBindings()
}
func getMusixmatchTokenFromDebugInfo(_ debugInfo: String) -> String? {
if let match = debugInfo.firstMatch("\\[UserToken\\]: ([a-f0-9]+)"),
let tokenRange = Range(match.range(at: 1), in: debugInfo) {
return String(debugInfo[tokenRange])
}
return nil
}
func getMusixmatchToken(_ input: String) -> String? {
if input ~= "^[a-f0-9]{54}$" {
return input
}
return nil
}
func requestAnonymousMusixmatchToken() {
isRequestingMusixmatchToken = true
AnonymousTokenHelper.requestAnonymousMusixmatchToken()
.sink(receiveCompletion: { [weak self] completion in
self?.isRequestingMusixmatchToken = false
switch completion {
case .failure(_):
self?.musixmatchTokenInputAlertPublisher.send(false)
case .finished:
UserDefaults.lyricsSource = .musixmatch
break
}
}, receiveValue: { [weak self] token in
self?.musixmatchToken = token
})
.store(in: &cancellables)
}
}
@@ -0,0 +1,25 @@
import SwiftUI
struct DynamicIconModifier: ViewModifier {
var systemName: String
var color: Color
@Binding var condition: Bool
func body(content: Content) -> some View {
HStack {
if condition {
Image(systemName: systemName)
.font(.title2)
.foregroundColor(color)
}
content
}
}
}
extension View {
func icon(_ systemName: String, color: Color, when condition: Binding<Bool>) -> some View {
modifier(DynamicIconModifier(systemName: systemName, color: color, condition: condition))
}
}
@@ -0,0 +1,101 @@
import SwiftUI
extension EeveeLyricsSettingsView {
private func lyricsSourceFooter() -> some View {
var text = "lyrics_source_description".localized
text.append("\n\n")
text.append("petitlyrics_description".localized)
text.append("\n\n")
text.append("lyrics_additional_info".localized)
return Text(text)
}
@ViewBuilder func lyricsSourceSection() -> some View {
Section {
Toggle(
"do_not_replace_lyrics".localized,
isOn: Binding<Bool>(
get: { viewModel.lyricsSource == .notReplaced },
set: {
viewModel.lyricsSource = $0
? .notReplaced
: LyricsSource.defaultSource
}
)
)
} footer: {
Text("restart_is_required_description".localized)
}
if viewModel.lyricsSource.isReplacingLyrics {
Section(footer: lyricsSourceFooter()) {
Picker(
"lyrics_source".localized,
selection: $viewModel.lyricsSource
) {
ForEach(LyricsSource.allCases, id: \.self) { lyricsSource in
Text(lyricsSource.description).tag(lyricsSource)
}
}
if viewModel.lyricsSource == .musixmatch {
musixmatchTokenField()
}
if viewModel.lyricsSource == .lrclib {
lrclibURLField()
}
}
}
}
@ViewBuilder private func musixmatchTokenField() -> some View {
VStack(alignment: .leading, spacing: 5) {
Text("musixmatch_user_token".localized)
TextField("user_token_placeholder".localized, text: $viewModel.musixmatchToken)
.foregroundColor(.gray)
}
.icon(
"exclamationmark.circle",
color: .red,
when: Binding<Bool>(
get: { !viewModel.isMusixmatchTokenValid },
set: { _ in }
)
)
.frame(maxWidth: .infinity, alignment: .leading)
}
@ViewBuilder private func lrclibURLField() -> some View {
VStack(alignment: .leading, spacing: 5) {
Text("lrclib_api".localized)
TextField(LrclibLyricsRepository.originalApiUrl, text: $viewModel.lyricsOptions.lrclibUrl)
.foregroundColor(.gray)
}
.icon(
"exclamationmark.circle",
color: .red,
when: Binding<Bool>(
get: {
viewModel.lrclibURLState == .invalidURL
|| viewModel.lrclibURLState == .unreachableURL
},
set: { _ in }
)
)
.icon(
"checkmark.seal",
color: .green,
when: Binding<Bool>(
get: { viewModel.lrclibURLState == .originalURL },
set: { _ in }
)
)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
@@ -0,0 +1,49 @@
import SwiftUI
extension EeveeLyricsSettingsView {
func showMusixmatchTokenAlert(_ oldSource: LyricsSource, _ showAnonymousTokenOption: Bool) {
var message = "enter_user_token_message".localized
if showAnonymousTokenOption {
message.append("\n\n")
message.append("request_anonymous_token_description".localized)
}
let alert = UIAlertController(
title: "enter_user_token".localized,
message: message,
preferredStyle: .alert
)
alert.addTextField() { textField in
textField.placeholder = "---- Debug Info ---- [Device]: \(UIDevice.current.isIpad ? "iPad" : "iPhone")"
}
alert.addAction(UIAlertAction(title: "Cancel".uiKitLocalized, style: .cancel) { _ in
viewModel.lyricsSource = oldSource
})
if showAnonymousTokenOption {
alert.addAction(UIAlertAction(title: "request_anonymous_token".localized, style: .default) { _ in
viewModel.requestAnonymousMusixmatchToken()
})
}
alert.addAction(UIAlertAction(title: "OK".uiKitLocalized, style: .default) { _ in
let text = alert.textFields!.first!.text!
guard let token =
viewModel.getMusixmatchTokenFromDebugInfo(text)
?? viewModel.getMusixmatchToken(text)
else {
viewModel.lyricsSource = oldSource
return
}
viewModel.musixmatchToken = token
UserDefaults.lyricsSource = .musixmatch
})
WindowHelper.shared.present(alert)
}
}
@@ -0,0 +1,82 @@
import SwiftUI
struct EeveeLyricsSettingsView: View {
@StateObject var viewModel = EeveeLyricsSettingsViewModel()
var body: some View {
List {
lyricsSourceSection()
if viewModel.lyricsSource != .notReplaced {
if viewModel.lyricsSource != .genius {
geniusFallbackSection()
}
romanizedLyricsSection()
if viewModel.lyricsSource == .musixmatch {
musixmatchLanguageSection()
}
}
NonIPadSpacerView()
}
.onReceive(viewModel.musixmatchTokenInputAlertPublisher) { showAnonymousTokenOption in
showMusixmatchTokenAlert(UserDefaults.lyricsSource, showAnonymousTokenOption)
}
.listStyle(GroupedListStyle())
.disabled(viewModel.isRequestingMusixmatchToken)
.animation(.default, value: viewModel.animationValues)
}
@ViewBuilder private func geniusFallbackSection() -> some View {
Section {
Toggle(
"genius_fallback".localized,
isOn: $viewModel.lyricsOptions.geniusFallback
)
if viewModel.lyricsOptions.geniusFallback {
Toggle(
"show_fallback_reasons".localized,
isOn: $viewModel.lyricsOptions.showFallbackReasons
)
}
} footer: {
Text("genius_fallback_description"
.localizeWithFormat(viewModel.lyricsSource.description))
}
}
@ViewBuilder private func romanizedLyricsSection() -> some View {
Section {
Toggle(
"romanized_lyrics".localized,
isOn: $viewModel.lyricsOptions.romanization
)
} footer: {
Text("romanized_lyrics_description".localized)
}
}
@ViewBuilder private func musixmatchLanguageSection() -> some View {
Section {
HStack {
Text("musixmatch_language".localized)
Spacer()
TextField("en", text: $viewModel.lyricsOptions.musixmatchLanguage)
.frame(maxWidth: 20)
.foregroundColor(.gray)
}
.icon(
"exclamationmark.triangle.fill",
color: .yellow,
when: $viewModel.showMusixmatchInvalidLanguageWarning
)
} footer: {
Text("musixmatch_language_description".localized)
}
}
}
@@ -45,12 +45,7 @@ struct EeveePatchingSettingsView: View {
}
}
if !UIDevice.current.isIpad {
Spacer()
.frame(height: 40)
.listRowBackground(Color.clear)
.modifier(ListRowSeparatorHidden())
}
NonIPadSpacerView()
}
.listStyle(GroupedListStyle())
.animation(.default, value: patchType)
@@ -3,7 +3,7 @@ import SwiftUI
struct ChevronRightView: View {
var body: some View {
Image(systemName: "chevron.right")
.font(.subheadline.weight(.semibold))
.foregroundColor(Color(UIColor.systemGray2))
.font(.system(size: 14, weight: .bold))
.foregroundColor(Color(UIColor.tertiaryLabel))
}
}
@@ -0,0 +1,12 @@
import SwiftUI
struct NonIPadSpacerView: View {
var body: some View {
if !UIDevice.current.isIpad {
Spacer()
.frame(height: 40)
.listRowBackground(Color.clear)
.modifier(ListRowSeparatorHidden())
}
}
}
@@ -6,7 +6,7 @@ struct EeveeUISettingsView: View {
var body: some View {
List {
if UserDefaults.lyricsSource.isReplacing {
if UserDefaults.lyricsSource.isReplacingLyrics {
Section(
header: Text("lyrics_background_color_section".localized),
footer: Text("lyrics_background_color_section_description".localized)
@@ -58,12 +58,7 @@ struct EeveeUISettingsView: View {
)
}
if !UIDevice.current.isIpad {
Spacer()
.frame(height: 40)
.listRowBackground(Color.clear)
.modifier(ListRowSeparatorHidden())
}
NonIPadSpacerView()
}
.listStyle(GroupedListStyle())
@@ -2,7 +2,6 @@ import SwiftUI
import UIKit
class EeveeSettingsViewController: SPTPageViewController {
let frame: CGRect
let settingsView: AnyView
@@ -1,9 +1,7 @@
import UIKit
class SPTPageViewController: UIViewController {
override func conforms(to aProtocol: Protocol) -> Bool {
if NSStringFromProtocol(aProtocol) ~= "SPTPageController" {
return true
}
@@ -1,72 +0,0 @@
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])
}
else if input ~= "^[a-f0-9]+$" {
return input
}
return nil
}
func showMusixmatchTokenAlert(_ oldSource: LyricsSource, showAnonymousTokenOption: Bool) {
var message = "enter_user_token_message".localized
if showAnonymousTokenOption {
message.append("\n\n")
message.append("request_anonymous_token_description".localized)
}
let alert = UIAlertController(
title: "enter_user_token".localized,
message: message,
preferredStyle: .alert
)
alert.addTextField() { textField in
textField.placeholder = "---- Debug Info ---- [Device]: iPhone"
}
alert.addAction(UIAlertAction(title: "Cancel".uiKitLocalized, style: .cancel) { _ in
lyricsSource = oldSource
})
if showAnonymousTokenOption {
alert.addAction(UIAlertAction(title: "request_anonymous_token".localized, style: .default) { _ in
Task {
defer {
isRequestingMusixmatchToken.toggle()
}
do {
isRequestingMusixmatchToken.toggle()
musixmatchToken = try await AnonymousTokenHelper.requestAnonymousMusixmatchToken()
UserDefaults.lyricsSource = .musixmatch
}
catch {
showMusixmatchTokenAlert(oldSource, showAnonymousTokenOption: false)
}
}
})
}
alert.addAction(UIAlertAction(title: "OK".uiKitLocalized, style: .default) { _ in
let text = alert.textFields!.first!.text!
guard let token = getMusixmatchToken(text) else {
lyricsSource = oldSource
return
}
musixmatchToken = token
UserDefaults.lyricsSource = .musixmatch
})
WindowHelper.shared.present(alert)
}
}
@@ -1,71 +0,0 @@
import SwiftUI
extension EeveeLyricsSettingsView {
func lyricsSourceFooter() -> some View {
var text = "lyrics_source_description".localized
text.append("\n\n")
text.append("petitlyrics_description".localized)
text.append("\n\n")
text.append("lyrics_additional_info".localized)
return Text(text)
}
@ViewBuilder func LyricsSourceSection() -> some View {
Section(footer: Text("restart_is_required_description".localized)) {
Toggle(
"do_not_replace_lyrics".localized,
isOn: Binding<Bool>(
get: { lyricsSource == .notReplaced },
set: { lyricsSource = $0 ? .notReplaced : LyricsSource.defaultSource }
)
)
}
.onChange(of: lyricsSource) { [lyricsSource] newSource in
if newSource == .musixmatch && musixmatchToken.isEmpty {
showMusixmatchTokenAlert(lyricsSource, showAnonymousTokenOption: true)
return
}
UserDefaults.lyricsSource = newSource
}
if lyricsSource.isReplacing {
Section(footer: lyricsSourceFooter()) {
Picker(
"lyrics_source".localized,
selection: $lyricsSource
) {
ForEach(LyricsSource.allCases, id: \.self) { lyricsSource in
Text(lyricsSource.description).tag(lyricsSource)
}
}
if lyricsSource == .musixmatch {
VStack(alignment: .leading, spacing: 5) {
Text("musixmatch_user_token".localized)
TextField("user_token_placeholder".localized, text: $musixmatchToken)
.foregroundColor(.gray)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.onChange(of: musixmatchToken) { input in
if input.isEmpty {
return
}
if let token = getMusixmatchToken(input) {
UserDefaults.musixmatchToken = token
self.musixmatchToken = token
}
else {
self.musixmatchToken = ""
}
}
}
}
}
@@ -1,106 +0,0 @@
import SwiftUI
struct EeveeLyricsSettingsView: View {
@State var musixmatchToken = UserDefaults.musixmatchToken
@State var lyricsSource = UserDefaults.lyricsSource
@State var geniusFallback = UserDefaults.geniusFallback
@State private var lyricsOptions = UserDefaults.lyricsOptions
@State var isRequestingMusixmatchToken = false
@State private var isShowingLanguageWarning = false
var body: some View {
List {
LyricsSourceSection()
if lyricsSource != .notReplaced {
if lyricsSource != .genius {
Section(
footer: Text("genius_fallback_description".localizeWithFormat(lyricsSource.description))
) {
Toggle(
"genius_fallback".localized,
isOn: $geniusFallback
)
if geniusFallback {
Toggle(
"show_fallback_reasons".localized,
isOn: Binding<Bool>(
get: { UserDefaults.fallbackReasons },
set: { UserDefaults.fallbackReasons = $0 }
)
)
}
}
}
//
Section(footer: Text("romanized_lyrics_description".localized)) {
Toggle(
"romanized_lyrics".localized,
isOn: $lyricsOptions.romanization
)
}
if lyricsSource == .musixmatch {
Section {
HStack {
if isShowingLanguageWarning {
Image(systemName: "exclamationmark.triangle")
.font(.title3)
.foregroundColor(.yellow)
}
Text("musixmatch_language".localized)
Spacer()
TextField("en", text: $lyricsOptions.musixmatchLanguage)
.frame(maxWidth: 20)
.foregroundColor(.gray)
}
} footer: {
Text("musixmatch_language_description".localized)
}
}
}
if !UIDevice.current.isIpad {
Spacer()
.frame(height: 40)
.listRowBackground(Color.clear)
.modifier(ListRowSeparatorHidden())
}
}
.listStyle(GroupedListStyle())
.disabled(isRequestingMusixmatchToken)
.animation(.default, value: lyricsSource)
.animation(.default, value: isRequestingMusixmatchToken)
.animation(.default, value: isShowingLanguageWarning)
.animation(.default, value: geniusFallback)
.onChange(of: geniusFallback) { geniusFallback in
UserDefaults.geniusFallback = geniusFallback
}
.onChange(of: lyricsOptions) { lyricsOptions in
let selectedLanguage = lyricsOptions.musixmatchLanguage
if selectedLanguage.isEmpty || selectedLanguage ~= "^[\\w\\d]{2}$" {
isShowingLanguageWarning = false
MusixmatchLyricsRepository.shared.selectedLanguage = selectedLanguage
UserDefaults.lyricsOptions = lyricsOptions
return
}
isShowingLanguageWarning = true
}
}
}
+1 -1
View File
@@ -24,7 +24,7 @@ struct EeveeSpotify: Tweak {
PremiumPatchingGroup().activate()
}
if UserDefaults.lyricsSource.isReplacing {
if UserDefaults.lyricsSource.isReplacingLyrics {
LyricsGroup().activate()
}
}
@@ -113,3 +113,5 @@ contributors = "Contributors";
request_anonymous_token = "Request Anonymous Token";
request_anonymous_token_description = "Tap “Request Anonymous Token” to request a token from Musixmatch without authorization.";
lrclib_api = "Server Address";
@@ -111,3 +111,5 @@ contributors = "Участники";
request_anonymous_token = "Запросить анонимный токен";
request_anonymous_token_description = "Нажмите Запросить анонимный токен, чтобы запросить токен у Musixmatch без авторизации.";
lrclib_api = "Адрес сервера";