From 08037dbb61158904c9418e32794d05b6dede19fa Mon Sep 17 00:00:00 2001 From: eevee <94960726+whoeevee@users.noreply.github.com> Date: Wed, 12 Mar 2025 21:54:10 +0300 Subject: [PATCH] lrclib api option, many improvements and refactoring --- .../EeveeSpotify/Lyrics/CustomLyrics.x.swift | 21 ++-- .../Models/Settings/LyricsOptions.swift | 5 +- .../Lyrics/Models/Settings/LyricsSource.swift | 2 +- .../Repositories/GeniusLyricsRepository.swift | 2 +- .../Repositories/LrclibLyricsRepository.swift | 15 ++- .../Repositories/PetitLyricsRepository.swift | 2 +- .../Models/Extensions/String+Extension.swift | 2 +- .../Extensions/UserDefaults+Extension.swift | 25 +---- .../EeveeSpotify/Models/Headers/SPTURL.swift | 1 + .../Premium/ServerSidedReminder.x.swift | 8 +- .../{ => GitHub}/Helpers/GitHubHelper.swift | 0 .../Models/EeveeContributor.swift | 0 .../Models/EeveeContributorSection.swift | 0 .../{ => GitHub}/Models/GitHubRelease.swift | 0 .../{ => GitHub}/Models/GitHubUser.swift | 0 .../Views}/EeveeContributorView.swift | 0 .../Views}/EeveeContributorsSheetView.swift | 0 .../Helpers/AnonymousTokenHelper.swift | 22 ---- .../Lyrics/Helpers/AnonymousTokenHelper.swift | 25 +++++ .../Models/AnonymousTokenError.swift | 0 .../Lyrics/Models/LrclibURLState.swift | 7 ++ ...yricsSettingsViewModel+setupBindings.swift | 81 +++++++++++++ .../EeveeLyricsSettingsViewModel.swift | 72 ++++++++++++ .../ViewModifiers/DynamicIconModifier.swift | 25 +++++ ...ricsSettingsView+lyricsSourceSection.swift | 101 +++++++++++++++++ ...ettingsView+showMusixmatchTokenAlert.swift | 49 ++++++++ .../Views/EeveeLyricsSettingsView.swift | 82 ++++++++++++++ .../Views}/EeveePatchingSettingsView.swift | 7 +- .../ViewModels/ImageViewModel.swift | 0 .../ListRowSeparatorHidden.swift | 0 .../Views}/ChevronRightView.swift | 4 +- .../Shared => Shared/Views}/ImageView.swift | 0 .../Shared/Views/NonIPadSpacerView.swift | 12 ++ .../Views}/EeveeUISettingsView.swift | 9 +- .../EeveeSettingsViewController.swift | 1 - .../SPTPageViewController.swift | 2 - .../{Shared => }/CommonIssuesTipView.swift | 0 .../{Shared => }/NavigationSectionView.swift | 0 .../EeveeLyricsSettingsView+Extension.swift | 72 ------------ ...ricsSettingsView+LyricsSourceSection.swift | 71 ------------ .../Sections/EeveeLyricsSettingsView.swift | 106 ------------------ Sources/EeveeSpotify/Tweak.x.swift | 2 +- .../en.lproj/Localizable.strings | 2 + .../ru.lproj/Localizable.strings | 2 + 44 files changed, 505 insertions(+), 332 deletions(-) rename Sources/EeveeSpotify/Settings/{ => GitHub}/Helpers/GitHubHelper.swift (100%) rename Sources/EeveeSpotify/Settings/{ => GitHub}/Models/EeveeContributor.swift (100%) rename Sources/EeveeSpotify/Settings/{ => GitHub}/Models/EeveeContributorSection.swift (100%) rename Sources/EeveeSpotify/Settings/{ => GitHub}/Models/GitHubRelease.swift (100%) rename Sources/EeveeSpotify/Settings/{ => GitHub}/Models/GitHubUser.swift (100%) rename Sources/EeveeSpotify/Settings/{Views/Contributors => GitHub/Views}/EeveeContributorView.swift (100%) rename Sources/EeveeSpotify/Settings/{Views/Contributors => GitHub/Views}/EeveeContributorsSheetView.swift (100%) delete mode 100644 Sources/EeveeSpotify/Settings/Helpers/AnonymousTokenHelper.swift create mode 100644 Sources/EeveeSpotify/Settings/Lyrics/Helpers/AnonymousTokenHelper.swift rename Sources/EeveeSpotify/Settings/{ => Lyrics}/Models/AnonymousTokenError.swift (100%) create mode 100644 Sources/EeveeSpotify/Settings/Lyrics/Models/LrclibURLState.swift create mode 100644 Sources/EeveeSpotify/Settings/Lyrics/ViewModels/EeveeLyricsSettingsViewModel+setupBindings.swift create mode 100644 Sources/EeveeSpotify/Settings/Lyrics/ViewModels/EeveeLyricsSettingsViewModel.swift create mode 100644 Sources/EeveeSpotify/Settings/Lyrics/ViewModifiers/DynamicIconModifier.swift create mode 100644 Sources/EeveeSpotify/Settings/Lyrics/Views/EeveeLyricsSettingsView+lyricsSourceSection.swift create mode 100644 Sources/EeveeSpotify/Settings/Lyrics/Views/EeveeLyricsSettingsView+showMusixmatchTokenAlert.swift create mode 100644 Sources/EeveeSpotify/Settings/Lyrics/Views/EeveeLyricsSettingsView.swift rename Sources/EeveeSpotify/Settings/{Views/Sections => Patching/Views}/EeveePatchingSettingsView.swift (88%) rename Sources/EeveeSpotify/Settings/{ => Shared}/ViewModels/ImageViewModel.swift (100%) rename Sources/EeveeSpotify/Settings/{ => Shared}/ViewModifiers/ListRowSeparatorHidden.swift (100%) rename Sources/EeveeSpotify/Settings/{Views/Shared => Shared/Views}/ChevronRightView.swift (52%) rename Sources/EeveeSpotify/Settings/{Views/Shared => Shared/Views}/ImageView.swift (100%) create mode 100644 Sources/EeveeSpotify/Settings/Shared/Views/NonIPadSpacerView.swift rename Sources/EeveeSpotify/Settings/{Views/Sections => UI/Views}/EeveeUISettingsView.swift (89%) rename Sources/EeveeSpotify/Settings/Views/{Shared => }/CommonIssuesTipView.swift (100%) rename Sources/EeveeSpotify/Settings/Views/{Shared => }/NavigationSectionView.swift (100%) delete mode 100644 Sources/EeveeSpotify/Settings/Views/Sections/EeveeLyricsSettingsView+Extension.swift delete mode 100644 Sources/EeveeSpotify/Settings/Views/Sections/EeveeLyricsSettingsView+LyricsSourceSection.swift delete mode 100644 Sources/EeveeSpotify/Settings/Views/Sections/EeveeLyricsSettingsView.swift diff --git a/Sources/EeveeSpotify/Lyrics/CustomLyrics.x.swift b/Sources/EeveeSpotify/Lyrics/CustomLyrics.x.swift index 2bd367a..35dc004 100644 --- a/Sources/EeveeSpotify/Lyrics/CustomLyrics.x.swift +++ b/Sources/EeveeSpotify/Lyrics/CustomLyrics.x.swift @@ -96,8 +96,10 @@ class LyricsOnlyViewControllerHook: ClassHook { // - 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 { } if lastLyricsState.wasRomanized { - var attributedString = Dynamic.SPTEncoreAttributedString.alloc( + let attributedString = Dynamic.SPTEncoreAttributedString.alloc( interface: SPTEncoreAttributedString.self ) @@ -146,6 +148,11 @@ class LyricsOnlyViewControllerHook: ClassHook { // +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 } diff --git a/Sources/EeveeSpotify/Lyrics/Models/Settings/LyricsOptions.swift b/Sources/EeveeSpotify/Lyrics/Models/Settings/LyricsOptions.swift index 7399edf..73bc645 100644 --- a/Sources/EeveeSpotify/Lyrics/Models/Settings/LyricsOptions.swift +++ b/Sources/EeveeSpotify/Lyrics/Models/Settings/LyricsOptions.swift @@ -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 } diff --git a/Sources/EeveeSpotify/Lyrics/Models/Settings/LyricsSource.swift b/Sources/EeveeSpotify/Lyrics/Models/Settings/LyricsSource.swift index d6ed810..2944289 100644 --- a/Sources/EeveeSpotify/Lyrics/Models/Settings/LyricsSource.swift +++ b/Sources/EeveeSpotify/Lyrics/Models/Settings/LyricsSource.swift @@ -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") diff --git a/Sources/EeveeSpotify/Lyrics/Repositories/GeniusLyricsRepository.swift b/Sources/EeveeSpotify/Lyrics/Repositories/GeniusLyricsRepository.swift index 22f04c9..8f33c54 100644 --- a/Sources/EeveeSpotify/Lyrics/Repositories/GeniusLyricsRepository.swift +++ b/Sources/EeveeSpotify/Lyrics/Repositories/GeniusLyricsRepository.swift @@ -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 diff --git a/Sources/EeveeSpotify/Lyrics/Repositories/LrclibLyricsRepository.swift b/Sources/EeveeSpotify/Lyrics/Repositories/LrclibLyricsRepository.swift index f0c80b3..407df3b 100644 --- a/Sources/EeveeSpotify/Lyrics/Repositories/LrclibLyricsRepository.swift +++ b/Sources/EeveeSpotify/Lyrics/Repositories/LrclibLyricsRepository.swift @@ -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) { diff --git a/Sources/EeveeSpotify/Lyrics/Repositories/PetitLyricsRepository.swift b/Sources/EeveeSpotify/Lyrics/Repositories/PetitLyricsRepository.swift index d68efdb..3515c66 100644 --- a/Sources/EeveeSpotify/Lyrics/Repositories/PetitLyricsRepository.swift +++ b/Sources/EeveeSpotify/Lyrics/Repositories/PetitLyricsRepository.swift @@ -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 diff --git a/Sources/EeveeSpotify/Models/Extensions/String+Extension.swift b/Sources/EeveeSpotify/Models/Extensions/String+Extension.swift index 0e066f3..1c38fbe 100644 --- a/Sources/EeveeSpotify/Models/Extensions/String+Extension.swift +++ b/Sources/EeveeSpotify/Models/Extensions/String+Extension.swift @@ -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) } diff --git a/Sources/EeveeSpotify/Models/Extensions/UserDefaults+Extension.swift b/Sources/EeveeSpotify/Models/Extensions/UserDefaults+Extension.swift index 9604450..bc68da6 100644 --- a/Sources/EeveeSpotify/Models/Extensions/UserDefaults+Extension.swift +++ b/Sources/EeveeSpotify/Models/Extensions/UserDefaults+Extension.swift @@ -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 { diff --git a/Sources/EeveeSpotify/Models/Headers/SPTURL.swift b/Sources/EeveeSpotify/Models/Headers/SPTURL.swift index bda67ab..7eba52a 100644 --- a/Sources/EeveeSpotify/Models/Headers/SPTURL.swift +++ b/Sources/EeveeSpotify/Models/Headers/SPTURL.swift @@ -3,4 +3,5 @@ import Foundation // it's NSURL actually, just with Spotify extensions @objc protocol SPTURL { func spt_trackIdentifier() -> String + func isPlaylistURL() -> Bool } diff --git a/Sources/EeveeSpotify/Premium/ServerSidedReminder.x.swift b/Sources/EeveeSpotify/Premium/ServerSidedReminder.x.swift index d51347a..b5bb3f2 100644 --- a/Sources/EeveeSpotify/Premium/ServerSidedReminder.x.swift +++ b/Sources/EeveeSpotify/Premium/ServerSidedReminder.x.swift @@ -26,11 +26,9 @@ class ContentOffliningUIHelperImplementationHook: ClassHook { 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, diff --git a/Sources/EeveeSpotify/Settings/Helpers/GitHubHelper.swift b/Sources/EeveeSpotify/Settings/GitHub/Helpers/GitHubHelper.swift similarity index 100% rename from Sources/EeveeSpotify/Settings/Helpers/GitHubHelper.swift rename to Sources/EeveeSpotify/Settings/GitHub/Helpers/GitHubHelper.swift diff --git a/Sources/EeveeSpotify/Settings/Models/EeveeContributor.swift b/Sources/EeveeSpotify/Settings/GitHub/Models/EeveeContributor.swift similarity index 100% rename from Sources/EeveeSpotify/Settings/Models/EeveeContributor.swift rename to Sources/EeveeSpotify/Settings/GitHub/Models/EeveeContributor.swift diff --git a/Sources/EeveeSpotify/Settings/Models/EeveeContributorSection.swift b/Sources/EeveeSpotify/Settings/GitHub/Models/EeveeContributorSection.swift similarity index 100% rename from Sources/EeveeSpotify/Settings/Models/EeveeContributorSection.swift rename to Sources/EeveeSpotify/Settings/GitHub/Models/EeveeContributorSection.swift diff --git a/Sources/EeveeSpotify/Settings/Models/GitHubRelease.swift b/Sources/EeveeSpotify/Settings/GitHub/Models/GitHubRelease.swift similarity index 100% rename from Sources/EeveeSpotify/Settings/Models/GitHubRelease.swift rename to Sources/EeveeSpotify/Settings/GitHub/Models/GitHubRelease.swift diff --git a/Sources/EeveeSpotify/Settings/Models/GitHubUser.swift b/Sources/EeveeSpotify/Settings/GitHub/Models/GitHubUser.swift similarity index 100% rename from Sources/EeveeSpotify/Settings/Models/GitHubUser.swift rename to Sources/EeveeSpotify/Settings/GitHub/Models/GitHubUser.swift diff --git a/Sources/EeveeSpotify/Settings/Views/Contributors/EeveeContributorView.swift b/Sources/EeveeSpotify/Settings/GitHub/Views/EeveeContributorView.swift similarity index 100% rename from Sources/EeveeSpotify/Settings/Views/Contributors/EeveeContributorView.swift rename to Sources/EeveeSpotify/Settings/GitHub/Views/EeveeContributorView.swift diff --git a/Sources/EeveeSpotify/Settings/Views/Contributors/EeveeContributorsSheetView.swift b/Sources/EeveeSpotify/Settings/GitHub/Views/EeveeContributorsSheetView.swift similarity index 100% rename from Sources/EeveeSpotify/Settings/Views/Contributors/EeveeContributorsSheetView.swift rename to Sources/EeveeSpotify/Settings/GitHub/Views/EeveeContributorsSheetView.swift diff --git a/Sources/EeveeSpotify/Settings/Helpers/AnonymousTokenHelper.swift b/Sources/EeveeSpotify/Settings/Helpers/AnonymousTokenHelper.swift deleted file mode 100644 index 6aa0d4f..0000000 --- a/Sources/EeveeSpotify/Settings/Helpers/AnonymousTokenHelper.swift +++ /dev/null @@ -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 - } -} diff --git a/Sources/EeveeSpotify/Settings/Lyrics/Helpers/AnonymousTokenHelper.swift b/Sources/EeveeSpotify/Settings/Lyrics/Helpers/AnonymousTokenHelper.swift new file mode 100644 index 0000000..d87411d --- /dev/null +++ b/Sources/EeveeSpotify/Settings/Lyrics/Helpers/AnonymousTokenHelper.swift @@ -0,0 +1,25 @@ +import UIKit +import Combine + +struct AnonymousTokenHelper { + private static let apiUrl = "https://apic.musixmatch.com" + + static func requestAnonymousMusixmatchToken() -> AnyPublisher { + 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() + } +} diff --git a/Sources/EeveeSpotify/Settings/Models/AnonymousTokenError.swift b/Sources/EeveeSpotify/Settings/Lyrics/Models/AnonymousTokenError.swift similarity index 100% rename from Sources/EeveeSpotify/Settings/Models/AnonymousTokenError.swift rename to Sources/EeveeSpotify/Settings/Lyrics/Models/AnonymousTokenError.swift diff --git a/Sources/EeveeSpotify/Settings/Lyrics/Models/LrclibURLState.swift b/Sources/EeveeSpotify/Settings/Lyrics/Models/LrclibURLState.swift new file mode 100644 index 0000000..21ecf57 --- /dev/null +++ b/Sources/EeveeSpotify/Settings/Lyrics/Models/LrclibURLState.swift @@ -0,0 +1,7 @@ +enum LrclibURLState { + case `default` + case invalidURL + case unreachableURL + case originalURL + case ok +} diff --git a/Sources/EeveeSpotify/Settings/Lyrics/ViewModels/EeveeLyricsSettingsViewModel+setupBindings.swift b/Sources/EeveeSpotify/Settings/Lyrics/ViewModels/EeveeLyricsSettingsViewModel+setupBindings.swift new file mode 100644 index 0000000..6afda5f --- /dev/null +++ b/Sources/EeveeSpotify/Settings/Lyrics/ViewModels/EeveeLyricsSettingsViewModel+setupBindings.swift @@ -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 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) + } +} diff --git a/Sources/EeveeSpotify/Settings/Lyrics/ViewModels/EeveeLyricsSettingsViewModel.swift b/Sources/EeveeSpotify/Settings/Lyrics/ViewModels/EeveeLyricsSettingsViewModel.swift new file mode 100644 index 0000000..bfd61f1 --- /dev/null +++ b/Sources/EeveeSpotify/Settings/Lyrics/ViewModels/EeveeLyricsSettingsViewModel.swift @@ -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() + 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() + + 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) + } +} diff --git a/Sources/EeveeSpotify/Settings/Lyrics/ViewModifiers/DynamicIconModifier.swift b/Sources/EeveeSpotify/Settings/Lyrics/ViewModifiers/DynamicIconModifier.swift new file mode 100644 index 0000000..ffda484 --- /dev/null +++ b/Sources/EeveeSpotify/Settings/Lyrics/ViewModifiers/DynamicIconModifier.swift @@ -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) -> some View { + modifier(DynamicIconModifier(systemName: systemName, color: color, condition: condition)) + } +} diff --git a/Sources/EeveeSpotify/Settings/Lyrics/Views/EeveeLyricsSettingsView+lyricsSourceSection.swift b/Sources/EeveeSpotify/Settings/Lyrics/Views/EeveeLyricsSettingsView+lyricsSourceSection.swift new file mode 100644 index 0000000..bff7086 --- /dev/null +++ b/Sources/EeveeSpotify/Settings/Lyrics/Views/EeveeLyricsSettingsView+lyricsSourceSection.swift @@ -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( + 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( + 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( + get: { + viewModel.lrclibURLState == .invalidURL + || viewModel.lrclibURLState == .unreachableURL + }, + set: { _ in } + ) + ) + .icon( + "checkmark.seal", + color: .green, + when: Binding( + get: { viewModel.lrclibURLState == .originalURL }, + set: { _ in } + ) + ) + .frame(maxWidth: .infinity, alignment: .leading) + } +} diff --git a/Sources/EeveeSpotify/Settings/Lyrics/Views/EeveeLyricsSettingsView+showMusixmatchTokenAlert.swift b/Sources/EeveeSpotify/Settings/Lyrics/Views/EeveeLyricsSettingsView+showMusixmatchTokenAlert.swift new file mode 100644 index 0000000..33fd6f1 --- /dev/null +++ b/Sources/EeveeSpotify/Settings/Lyrics/Views/EeveeLyricsSettingsView+showMusixmatchTokenAlert.swift @@ -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) + } +} diff --git a/Sources/EeveeSpotify/Settings/Lyrics/Views/EeveeLyricsSettingsView.swift b/Sources/EeveeSpotify/Settings/Lyrics/Views/EeveeLyricsSettingsView.swift new file mode 100644 index 0000000..a0203a7 --- /dev/null +++ b/Sources/EeveeSpotify/Settings/Lyrics/Views/EeveeLyricsSettingsView.swift @@ -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) + } + } +} diff --git a/Sources/EeveeSpotify/Settings/Views/Sections/EeveePatchingSettingsView.swift b/Sources/EeveeSpotify/Settings/Patching/Views/EeveePatchingSettingsView.swift similarity index 88% rename from Sources/EeveeSpotify/Settings/Views/Sections/EeveePatchingSettingsView.swift rename to Sources/EeveeSpotify/Settings/Patching/Views/EeveePatchingSettingsView.swift index 340ca2f..6268bd8 100644 --- a/Sources/EeveeSpotify/Settings/Views/Sections/EeveePatchingSettingsView.swift +++ b/Sources/EeveeSpotify/Settings/Patching/Views/EeveePatchingSettingsView.swift @@ -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) diff --git a/Sources/EeveeSpotify/Settings/ViewModels/ImageViewModel.swift b/Sources/EeveeSpotify/Settings/Shared/ViewModels/ImageViewModel.swift similarity index 100% rename from Sources/EeveeSpotify/Settings/ViewModels/ImageViewModel.swift rename to Sources/EeveeSpotify/Settings/Shared/ViewModels/ImageViewModel.swift diff --git a/Sources/EeveeSpotify/Settings/ViewModifiers/ListRowSeparatorHidden.swift b/Sources/EeveeSpotify/Settings/Shared/ViewModifiers/ListRowSeparatorHidden.swift similarity index 100% rename from Sources/EeveeSpotify/Settings/ViewModifiers/ListRowSeparatorHidden.swift rename to Sources/EeveeSpotify/Settings/Shared/ViewModifiers/ListRowSeparatorHidden.swift diff --git a/Sources/EeveeSpotify/Settings/Views/Shared/ChevronRightView.swift b/Sources/EeveeSpotify/Settings/Shared/Views/ChevronRightView.swift similarity index 52% rename from Sources/EeveeSpotify/Settings/Views/Shared/ChevronRightView.swift rename to Sources/EeveeSpotify/Settings/Shared/Views/ChevronRightView.swift index 56089b9..94e9baa 100644 --- a/Sources/EeveeSpotify/Settings/Views/Shared/ChevronRightView.swift +++ b/Sources/EeveeSpotify/Settings/Shared/Views/ChevronRightView.swift @@ -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)) } } diff --git a/Sources/EeveeSpotify/Settings/Views/Shared/ImageView.swift b/Sources/EeveeSpotify/Settings/Shared/Views/ImageView.swift similarity index 100% rename from Sources/EeveeSpotify/Settings/Views/Shared/ImageView.swift rename to Sources/EeveeSpotify/Settings/Shared/Views/ImageView.swift diff --git a/Sources/EeveeSpotify/Settings/Shared/Views/NonIPadSpacerView.swift b/Sources/EeveeSpotify/Settings/Shared/Views/NonIPadSpacerView.swift new file mode 100644 index 0000000..e1c488c --- /dev/null +++ b/Sources/EeveeSpotify/Settings/Shared/Views/NonIPadSpacerView.swift @@ -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()) + } + } +} diff --git a/Sources/EeveeSpotify/Settings/Views/Sections/EeveeUISettingsView.swift b/Sources/EeveeSpotify/Settings/UI/Views/EeveeUISettingsView.swift similarity index 89% rename from Sources/EeveeSpotify/Settings/Views/Sections/EeveeUISettingsView.swift rename to Sources/EeveeSpotify/Settings/UI/Views/EeveeUISettingsView.swift index ce4404e..d428e91 100644 --- a/Sources/EeveeSpotify/Settings/Views/Sections/EeveeUISettingsView.swift +++ b/Sources/EeveeSpotify/Settings/UI/Views/EeveeUISettingsView.swift @@ -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()) diff --git a/Sources/EeveeSpotify/Settings/ViewControllers/EeveeSettingsViewController.swift b/Sources/EeveeSpotify/Settings/ViewControllers/EeveeSettingsViewController.swift index 7dde787..9217618 100644 --- a/Sources/EeveeSpotify/Settings/ViewControllers/EeveeSettingsViewController.swift +++ b/Sources/EeveeSpotify/Settings/ViewControllers/EeveeSettingsViewController.swift @@ -2,7 +2,6 @@ import SwiftUI import UIKit class EeveeSettingsViewController: SPTPageViewController { - let frame: CGRect let settingsView: AnyView diff --git a/Sources/EeveeSpotify/Settings/ViewControllers/SPTPageViewController.swift b/Sources/EeveeSpotify/Settings/ViewControllers/SPTPageViewController.swift index 39abc34..0260b29 100644 --- a/Sources/EeveeSpotify/Settings/ViewControllers/SPTPageViewController.swift +++ b/Sources/EeveeSpotify/Settings/ViewControllers/SPTPageViewController.swift @@ -1,9 +1,7 @@ import UIKit class SPTPageViewController: UIViewController { - override func conforms(to aProtocol: Protocol) -> Bool { - if NSStringFromProtocol(aProtocol) ~= "SPTPageController" { return true } diff --git a/Sources/EeveeSpotify/Settings/Views/Shared/CommonIssuesTipView.swift b/Sources/EeveeSpotify/Settings/Views/CommonIssuesTipView.swift similarity index 100% rename from Sources/EeveeSpotify/Settings/Views/Shared/CommonIssuesTipView.swift rename to Sources/EeveeSpotify/Settings/Views/CommonIssuesTipView.swift diff --git a/Sources/EeveeSpotify/Settings/Views/Shared/NavigationSectionView.swift b/Sources/EeveeSpotify/Settings/Views/NavigationSectionView.swift similarity index 100% rename from Sources/EeveeSpotify/Settings/Views/Shared/NavigationSectionView.swift rename to Sources/EeveeSpotify/Settings/Views/NavigationSectionView.swift diff --git a/Sources/EeveeSpotify/Settings/Views/Sections/EeveeLyricsSettingsView+Extension.swift b/Sources/EeveeSpotify/Settings/Views/Sections/EeveeLyricsSettingsView+Extension.swift deleted file mode 100644 index 343346e..0000000 --- a/Sources/EeveeSpotify/Settings/Views/Sections/EeveeLyricsSettingsView+Extension.swift +++ /dev/null @@ -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) - } -} - diff --git a/Sources/EeveeSpotify/Settings/Views/Sections/EeveeLyricsSettingsView+LyricsSourceSection.swift b/Sources/EeveeSpotify/Settings/Views/Sections/EeveeLyricsSettingsView+LyricsSourceSection.swift deleted file mode 100644 index fbf1925..0000000 --- a/Sources/EeveeSpotify/Settings/Views/Sections/EeveeLyricsSettingsView+LyricsSourceSection.swift +++ /dev/null @@ -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( - 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 = "" - } - } - } - } -} diff --git a/Sources/EeveeSpotify/Settings/Views/Sections/EeveeLyricsSettingsView.swift b/Sources/EeveeSpotify/Settings/Views/Sections/EeveeLyricsSettingsView.swift deleted file mode 100644 index 3eefe9b..0000000 --- a/Sources/EeveeSpotify/Settings/Views/Sections/EeveeLyricsSettingsView.swift +++ /dev/null @@ -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( - 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 - } - } -} diff --git a/Sources/EeveeSpotify/Tweak.x.swift b/Sources/EeveeSpotify/Tweak.x.swift index 9668bbc..a84af45 100644 --- a/Sources/EeveeSpotify/Tweak.x.swift +++ b/Sources/EeveeSpotify/Tweak.x.swift @@ -24,7 +24,7 @@ struct EeveeSpotify: Tweak { PremiumPatchingGroup().activate() } - if UserDefaults.lyricsSource.isReplacing { + if UserDefaults.lyricsSource.isReplacingLyrics { LyricsGroup().activate() } } diff --git a/layout/Library/Application Support/EeveeSpotify.bundle/en.lproj/Localizable.strings b/layout/Library/Application Support/EeveeSpotify.bundle/en.lproj/Localizable.strings index 814664f..aff5c4a 100644 --- a/layout/Library/Application Support/EeveeSpotify.bundle/en.lproj/Localizable.strings +++ b/layout/Library/Application Support/EeveeSpotify.bundle/en.lproj/Localizable.strings @@ -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"; diff --git a/layout/Library/Application Support/EeveeSpotify.bundle/ru.lproj/Localizable.strings b/layout/Library/Application Support/EeveeSpotify.bundle/ru.lproj/Localizable.strings index 859a1cc..21b80cc 100644 --- a/layout/Library/Application Support/EeveeSpotify.bundle/ru.lproj/Localizable.strings +++ b/layout/Library/Application Support/EeveeSpotify.bundle/ru.lproj/Localizable.strings @@ -111,3 +111,5 @@ contributors = "Участники"; request_anonymous_token = "Запросить анонимный токен"; request_anonymous_token_description = "Нажмите Запросить анонимный токен, чтобы запросить токен у Musixmatch без авторизации."; + +lrclib_api = "Адрес сервера";