diff --git a/Localizable.xcstrings b/Localizable.xcstrings index b604e7b..a641a86 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -21,7 +21,8 @@ "value" : "" } } - } + }, + "shouldTranslate" : false }, " " : { "localizations" : { @@ -43,7 +44,8 @@ "value" : " " } } - } + }, + "shouldTranslate" : false }, "..." : { "localizations" : { @@ -91,7 +93,7 @@ } }, "[Lyric Fever GitHub](https://github.com/aviwad/LyricFever)\nVersion 3.2" : { - + "shouldTranslate" : false }, "%@s" : { "localizations" : { @@ -336,6 +338,7 @@ } }, "Arist Name" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -1215,6 +1218,9 @@ } } }, + "LRCLIB" : { + "shouldTranslate" : false + }, "Lyric Fever: Fullscreen" : { "localizations" : { "zh-Hans" : { @@ -1442,6 +1448,7 @@ } }, "More options" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -1464,6 +1471,7 @@ } }, "More options, AirPlay delay enabled" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -1485,6 +1493,28 @@ } } }, + "NetEase" : { + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "网易" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "網易" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "網易" + } + } + } + }, "Next" : { "localizations" : { "zh-Hans" : { @@ -2045,6 +2075,28 @@ } } }, + "Primary Lyric Source" : { + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "优先歌词源" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "優先歌詞源" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "優先歌詞源" + } + } + } + }, "Quit" : { "localizations" : { "zh-Hans" : { diff --git a/LyricFever/Support Files/UserDefaultStorage.swift b/LyricFever/Support Files/UserDefaultStorage.swift index 052d3e8..5b35b6a 100644 --- a/LyricFever/Support Files/UserDefaultStorage.swift +++ b/LyricFever/Support Files/UserDefaultStorage.swift @@ -26,6 +26,7 @@ class UserDefaultStorage { var romanize: Bool = false var romanizeMetadata: Bool = true var chinesePreference: Int = 0 + var lyricSourcePreference: String = "LRCLIB" #if os(macOS) var spotifyConnectDelayCount: Int = 400 var hasMigrated: Bool = false diff --git a/LyricFever/ViewModel.swift b/LyricFever/ViewModel.swift index 3fa43b9..157a1d1 100644 --- a/LyricFever/ViewModel.swift +++ b/LyricFever/ViewModel.swift @@ -53,9 +53,9 @@ import MediaRemoteAdapter // print("Apple Music Artwork Workaround: Ignoring artwork for existing song") // return // } else { -// self.appleMusicUniqueIdentifier = data.payload.uniqueIdentifier +// self.appleMuszxicUniqueIdentifier = data.payload.uniqueIdentifier // } - guard let artwork = data.payload.artwork else { + guard let artwork = data?.payload.artwork else { if self.currentlyPlaying == nil { self.artworkImage = nil } @@ -140,12 +140,17 @@ import MediaRemoteAdapter var displayKaraoke: Bool { get { - showLyrics && isPlaying && userDefaultStorage.karaoke && !karaokeModeHovering && (currentlyPlayingLyricsIndex != nil) + showLyrics && isPlaying && userDefaultStorage.karaoke && !karaokeModeHovering && (currentlyPlayingLyricsIndex != nil) && currentLyricIsNotBlank } set { } } + + private var currentLyricIsNotBlank: Bool { + guard let index = currentlyPlayingLyricsIndex, index < currentlyPlayingLyrics.count else { return false } + return !currentlyPlayingLyrics[index].words.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty + } var displayFullscreen: Bool { get { fullscreen @@ -279,10 +284,32 @@ import MediaRemoteAdapter #if os(macOS) var localFileUploadProvider = LocalFileUploadProvider() #endif - @ObservationIgnored lazy var allNetworkLyricProviders: [LyricProvider] = [spotifyLyricProvider, lRCLyricProvider, netEaseLyricProvider] + @ObservationIgnored var allNetworkLyricProviders: [LyricProvider] { + switch userDefaultStorage.lyricSourcePreference { + case "Spotify": + return [spotifyLyricProvider, lRCLyricProvider, netEaseLyricProvider] + case "NetEase": + return [netEaseLyricProvider, lRCLyricProvider, spotifyLyricProvider] + case "LRCLIB": + return [lRCLyricProvider, spotifyLyricProvider, netEaseLyricProvider] + default: + return [lRCLyricProvider, spotifyLyricProvider, netEaseLyricProvider] + } + } // custom order because LRCLIB is tweaking for the time being - @ObservationIgnored lazy var allNetworkLyricProvidersForSearch: [LyricProvider] = [spotifyLyricProvider, netEaseLyricProvider, lRCLyricProvider] + @ObservationIgnored var allNetworkLyricProvidersForSearch: [LyricProvider] { + switch userDefaultStorage.lyricSourcePreference { + case "Spotify": + return [spotifyLyricProvider, netEaseLyricProvider, lRCLyricProvider] + case "NetEase": + return [netEaseLyricProvider, spotifyLyricProvider, lRCLyricProvider] + case "LRCLIB": + return [lRCLyricProvider, spotifyLyricProvider, netEaseLyricProvider] + default: + return [lRCLyricProvider, spotifyLyricProvider, netEaseLyricProvider] + } + } var isFirstFetch = true @@ -785,7 +812,7 @@ import MediaRemoteAdapter func startLyricUpdater() { currentLyricsUpdaterTask?.cancel() - if !isPlaying || currentlyPlayingLyrics.isEmpty || mustUpdateUrgent { + if !isPlaying || currentlyPlayingLyrics.isEmpty { return } // If an index exists, we're unpausing: meaning we must instantly find the current lyric @@ -1141,14 +1168,24 @@ extension ViewModel { // Task cancelled means we're working with old song data, so dont update Spotify ID with old song's ID // search for equivalent spotify song - if let spotifyResult = try await musicToSpotifyHelper() { - self.currentlyPlayingName = spotifyResult.SpotifyName - self.currentlyPlayingArtist = spotifyResult.SpotifyArtist - self.currentAlbumName = spotifyResult.SpotifyAlbum - self.currentlyPlaying = spotifyResult.SpotifyID - } else { + do { + if let spotifyResult = try await musicToSpotifyHelper() { + self.currentlyPlayingName = spotifyResult.SpotifyName + self.currentlyPlayingArtist = spotifyResult.SpotifyArtist + self.currentAlbumName = spotifyResult.SpotifyAlbum + self.currentlyPlaying = spotifyResult.SpotifyID + } else { + if let alternativeID = appleMusicPlayer.alternativeID, alternativeID != "" { + try Task.checkCancellation() + self.currentlyPlaying = alternativeID + } else { + lyricsIsEmptyPostLoad = true + } + } + } catch { + print("Spotify lookup failed with error \(error). Falling back to Apple Music ID.") if let alternativeID = appleMusicPlayer.alternativeID, alternativeID != "" { - try Task.checkCancellation() + try? Task.checkCancellation() self.currentlyPlaying = alternativeID } else { lyricsIsEmptyPostLoad = true diff --git a/LyricFever/Views/KaraokeView/FloatingPanel.swift b/LyricFever/Views/KaraokeView/FloatingPanel.swift index 5125966..19a1a91 100644 --- a/LyricFever/Views/KaraokeView/FloatingPanel.swift +++ b/LyricFever/Views/KaraokeView/FloatingPanel.swift @@ -91,7 +91,7 @@ class FloatingPanel: NSPanel { standardWindowButton(.miniaturizeButton)?.isHidden = true standardWindowButton(.zoomButton)?.isHidden = true contentView = NSHostingView(rootView: view() - .preferredColorScheme(.dark) +// .preferredColorScheme(.dark) .environment(\.floatingPanel, self)) hasShadow = false } diff --git a/LyricFever/Views/KaraokeView/KaraokeView.swift b/LyricFever/Views/KaraokeView/KaraokeView.swift index d2231f4..d5dbf89 100644 --- a/LyricFever/Views/KaraokeView/KaraokeView.swift +++ b/LyricFever/Views/KaraokeView/KaraokeView.swift @@ -10,27 +10,15 @@ import SDWebImage import ColorKit import Combine -struct VisualEffectView: NSViewRepresentable { - func makeNSView(context: Context) -> NSVisualEffectView { - let view = NSVisualEffectView() - - view.blendingMode = .behindWindow - view.state = .active - view.material = .hudWindow - return view - } - - func updateNSView(_ nsView: NSVisualEffectView, context: Context) { - // - nsView.material = .hudWindow - nsView.blendingMode = .behindWindow - } -} - struct KaraokeView: View { @Environment(ViewModel.self) var viewmodel @AppStorage("karaokeTransparency") var karaokeTransparency: Double = 50 - + + // Adjust threshold here + private var useForcedWhiteText: Bool { + karaokeTransparency / 100 > 0.35 + } + func currentWords(for currentlyPlayingLyricsIndex: Int) -> String { if !viewmodel.romanizedLyrics.isEmpty { return viewmodel.romanizedLyrics[currentlyPlayingLyricsIndex] @@ -40,32 +28,29 @@ struct KaraokeView: View { return viewmodel.currentlyPlayingLyrics[currentlyPlayingLyricsIndex].words } } - + func multilingualView(_ currentlyPlayingLyricsIndex: Int) -> some View { VStack(spacing: 6) { Text(verbatim: currentWords(for: currentlyPlayingLyricsIndex)) - -// Text(verbatim: viewmodel.romanizedLyrics.isEmpty ? viewmodel.currentlyPlayingLyrics[currentlyPlayingLyricsIndex].words : viewmodel.romanizedLyrics[currentlyPlayingLyricsIndex]) Text(verbatim: viewmodel.translatedLyric[currentlyPlayingLyricsIndex]) - .font(.custom(viewmodel.karaokeFont.fontName, size: 0.9*(viewmodel.karaokeFont.pointSize))) + .font(.custom(viewmodel.karaokeFont.fontName, size: 0.9 * (viewmodel.karaokeFont.pointSize))) .opacity(0.85) } } - + func originalAndTranslationAreDifferent(for currentlyPlayingLyricsIndex: Int) -> Bool { viewmodel.currentlyPlayingLyrics[currentlyPlayingLyricsIndex].words != viewmodel.translatedLyric[currentlyPlayingLyricsIndex] } - + @ViewBuilder func lyricsView() -> some View { if let currentlyPlayingLyricsIndex = viewmodel.currentlyPlayingLyricsIndex { if viewmodel.translationExists { - if viewmodel.userDefaultStorage.karaokeShowMultilingual, originalAndTranslationAreDifferent(for: currentlyPlayingLyricsIndex) { + if viewmodel.userDefaultStorage.karaokeShowMultilingual, + originalAndTranslationAreDifferent(for: currentlyPlayingLyricsIndex) { multilingualView(currentlyPlayingLyricsIndex) -// .id(currentlyPlayingLyricsIndex) -// .compositingGroup() - } - else { + .compositingGroup() + } else { Text(verbatim: viewmodel.translatedLyric[currentlyPlayingLyricsIndex]) } } else { @@ -81,37 +66,66 @@ struct KaraokeView: View { Text("") } } - - @ViewBuilder - var finalKaraokeView: some View { - lyricsView() - .id(viewmodel.currentlyPlayingLyricsIndex) - .lineLimit(2) - .foregroundStyle(.white) - .minimumScaleFactor(0.9) - .font(.custom(viewmodel.karaokeFont.fontName, size: viewmodel.karaokeFont.pointSize)) - .padding(10) - .padding(.horizontal, 10) - .background { - viewmodel.currentAlbumArt - .transition(.opacity) - .opacity(karaokeTransparency/100) - } -// .drawingGroup() - .background( - VisualEffectView().ignoresSafeArea() - ) - .cornerRadius(16) - .onHover { hover in - if viewmodel.userDefaultStorage.karaokeModeHoveringSetting { - viewmodel.karaokeModeHovering = hover - } - } - .multilineTextAlignment(.center) - .frame(minWidth: 800, maxWidth: 800, minHeight: 100, maxHeight: 100, alignment: .center) - } - + var body: some View { - finalKaraokeView + if #available(macOS 26.0, *) { + // osscilate the lyrics view so the Liquid Glass rendering is always lively + TimelineView(.animation) { timeline in + let offset = 0.1 * sin(timeline.date.timeIntervalSinceReferenceDate * 20 * .pi) // adjust the 20 to change the frequency of the oscillation + lyricsView() + .lineLimit(2) + .foregroundStyle(useForcedWhiteText ? Color.white : Color.primary) + .minimumScaleFactor(0.9) + .font(.custom(viewmodel.karaokeFont.fontName, size: viewmodel.karaokeFont.pointSize)) + .padding(10) + .padding(.horizontal, 10) + .background { + if viewmodel.userDefaultStorage.karaokeUseAlbumColor, karaokeTransparency > 0 { + viewmodel.currentAlbumArt + .opacity(karaokeTransparency / 100) + } + } + .glassEffect(in: .rect(cornerRadius: 16)) + .cornerRadius(16) + .onHover { hover in + if viewmodel.userDefaultStorage.karaokeModeHoveringSetting { + viewmodel.karaokeModeHovering = hover + } + } + .multilineTextAlignment(.center) + .frame(minWidth: 800, maxWidth: 800, minHeight: 100, maxHeight: 100, alignment: .center) + .offset(y: offset) + } + } else { + lyricsView() + .lineLimit(2) + .foregroundStyle(useForcedWhiteText ? Color.white : Color.primary) + .minimumScaleFactor(0.9) + .font(.custom(viewmodel.karaokeFont.fontName, size: viewmodel.karaokeFont.pointSize)) + .padding(10) + .padding(.horizontal, 10) + .background { + // Material + optional album overlay + Rectangle() + .fill(Material.regular) + .overlay( + Group { + if viewmodel.userDefaultStorage.karaokeUseAlbumColor, karaokeTransparency > 0 { + viewmodel.currentAlbumArt + .opacity(karaokeTransparency / 100) + } + } + ) + .cornerRadius(16) + .ignoresSafeArea() + } + .onHover { hover in + if viewmodel.userDefaultStorage.karaokeModeHoveringSetting { + viewmodel.karaokeModeHovering = hover + } + } + .multilineTextAlignment(.center) + .frame(minWidth: 800, maxWidth: 800, minHeight: 100, maxHeight: 100, alignment: .center) + } } } diff --git a/LyricFever/Views/OnboardingView/MainSettingsView.swift b/LyricFever/Views/OnboardingView/MainSettingsView.swift index b76f206..e559b8b 100644 --- a/LyricFever/Views/OnboardingView/MainSettingsView.swift +++ b/LyricFever/Views/OnboardingView/MainSettingsView.swift @@ -145,6 +145,14 @@ struct MainSettingsView: View { permissionsOrNextButton .frame(height: 40) + + Picker("Primary Lyric Source", selection: $viewmodel.userDefaultStorage.lyricSourcePreference) { + Text("LRCLIB").tag("LRCLIB") + Text("Spotify").tag("Spotify") + Text("NetEase").tag("NetEase") + } + .pickerStyle(.menu) + .frame(width: 300) VStack { Text("Email me at [aviwad@gmail.com](mailto:aviwad@gmail.com) for any support")