From 965bd8da916045f5e2578a7d9fd1dfc60da07fef Mon Sep 17 00:00:00 2001 From: michaelqiu Date: Thu, 16 Oct 2025 22:00:40 +0800 Subject: [PATCH 01/17] Add Liquid Glass for KaraokeView for macOS26+ --- .../Views/KaraokeView/KaraokeView.swift | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/LyricFever/Views/KaraokeView/KaraokeView.swift b/LyricFever/Views/KaraokeView/KaraokeView.swift index db42441..23e2876 100644 --- a/LyricFever/Views/KaraokeView/KaraokeView.swift +++ b/LyricFever/Views/KaraokeView/KaraokeView.swift @@ -11,19 +11,27 @@ 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 makeNSView(context: Context) -> NSView { + if #available(macOS 26.0, *) { + let glassEffectView = NSGlassEffectView() + glassEffectView.style = NSGlassEffectView.Style.regular + return glassEffectView + } else { + let visualEffectView = NSVisualEffectView() + visualEffectView.blendingMode = .behindWindow + visualEffectView.state = .active + visualEffectView.material = .hudWindow + return visualEffectView + } } - func updateNSView(_ nsView: NSVisualEffectView, context: Context) { - // - nsView.material = .hudWindow - nsView.blendingMode = .behindWindow + func updateNSView(_ nsView: NSView, context: Context) { + if #available(macOS 26.0, *), let glassEffectView = nsView as? NSGlassEffectView { + glassEffectView.style = NSGlassEffectView.Style.regular + } else if let visualEffectView = nsView as? NSVisualEffectView { + visualEffectView.material = .hudWindow + visualEffectView.blendingMode = .behindWindow + } } } From 3a7236319f83cdaac63f13f34e8ca878bbe26749 Mon Sep 17 00:00:00 2001 From: "Michael Y. Qiu" Date: Thu, 16 Oct 2025 22:03:03 +0800 Subject: [PATCH 02/17] Add Liquid Glass for KaraokeView for macOS26+ --- .../Views/KaraokeView/KaraokeView.swift | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/LyricFever/Views/KaraokeView/KaraokeView.swift b/LyricFever/Views/KaraokeView/KaraokeView.swift index db42441..23e2876 100644 --- a/LyricFever/Views/KaraokeView/KaraokeView.swift +++ b/LyricFever/Views/KaraokeView/KaraokeView.swift @@ -11,19 +11,27 @@ 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 makeNSView(context: Context) -> NSView { + if #available(macOS 26.0, *) { + let glassEffectView = NSGlassEffectView() + glassEffectView.style = NSGlassEffectView.Style.regular + return glassEffectView + } else { + let visualEffectView = NSVisualEffectView() + visualEffectView.blendingMode = .behindWindow + visualEffectView.state = .active + visualEffectView.material = .hudWindow + return visualEffectView + } } - func updateNSView(_ nsView: NSVisualEffectView, context: Context) { - // - nsView.material = .hudWindow - nsView.blendingMode = .behindWindow + func updateNSView(_ nsView: NSView, context: Context) { + if #available(macOS 26.0, *), let glassEffectView = nsView as? NSGlassEffectView { + glassEffectView.style = NSGlassEffectView.Style.regular + } else if let visualEffectView = nsView as? NSVisualEffectView { + visualEffectView.material = .hudWindow + visualEffectView.blendingMode = .behindWindow + } } } From ebf56659ab1a73e23f02218ea19ec46e439cb4a6 Mon Sep 17 00:00:00 2001 From: "Michael Y. Qiu" Date: Sun, 19 Oct 2025 09:35:04 +0800 Subject: [PATCH 03/17] Use .primary for lyrics Remove preferred dark scheme for floating panel --- LyricFever/Views/KaraokeView/FloatingPanel.swift | 2 +- LyricFever/Views/KaraokeView/KaraokeView.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 23e2876..7b4fe27 100644 --- a/LyricFever/Views/KaraokeView/KaraokeView.swift +++ b/LyricFever/Views/KaraokeView/KaraokeView.swift @@ -93,7 +93,7 @@ struct KaraokeView: View { var finalKaraokeView: some View { lyricsView() .lineLimit(2) - .foregroundStyle(.white) + .foregroundStyle(.primary) .minimumScaleFactor(0.9) .font(.custom(viewmodel.karaokeFont.fontName, size: viewmodel.karaokeFont.pointSize)) .padding(10) From 490115ef0bafb091437bd21d39520a74e87fa382 Mon Sep 17 00:00:00 2001 From: "Michael Y. Qiu" Date: Wed, 5 Nov 2025 17:06:13 +0800 Subject: [PATCH 04/17] Calculate luminance to determine lyrics color --- .../Views/KaraokeView/KaraokeView.swift | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/LyricFever/Views/KaraokeView/KaraokeView.swift b/LyricFever/Views/KaraokeView/KaraokeView.swift index 7b4fe27..9ef8795 100644 --- a/LyricFever/Views/KaraokeView/KaraokeView.swift +++ b/LyricFever/Views/KaraokeView/KaraokeView.swift @@ -10,6 +10,18 @@ import SDWebImage import ColorKit import Combine +extension Color { + var perceivedBrightness: Double { + // Convert to sRGB components + let components = self.cgColor?.components + let r = Double(components?[0] ?? 0) + let g = Double(components?[1] ?? 0) + let b = Double(components?[2] ?? 0) + + return 0.2126 * r + 0.7152 * g + 0.0722 * b + } +} + struct VisualEffectView: NSViewRepresentable { func makeNSView(context: Context) -> NSView { if #available(macOS 26.0, *) { @@ -39,6 +51,21 @@ struct KaraokeView: View { @Environment(ViewModel.self) var viewmodel @AppStorage("karaokeTransparency") var karaokeTransparency: Double = 50 + private var effectiveTextColor: Color { + let transparency = karaokeTransparency / 100.0 + + // If fully transparent, rely on system appearance + if transparency <= 0.01 { + return .primary + } + + let bgColor = viewmodel.currentAlbumArt + let brightness = bgColor.perceivedBrightness + + let threshold: Double = 0.5 + return brightness > threshold ? .black : .white + } + func currentWords(for currentlyPlayingLyricsIndex: Int) -> String { if !viewmodel.romanizedLyrics.isEmpty { return viewmodel.romanizedLyrics[currentlyPlayingLyricsIndex] From 8256a5308ec15d40afbac7cbbc9559d660bf4e80 Mon Sep 17 00:00:00 2001 From: "Michael Y. Qiu" Date: Wed, 5 Nov 2025 17:07:30 +0800 Subject: [PATCH 05/17] Calculate contrast between albumColor and white/black then choose the larger one --- .../Views/KaraokeView/KaraokeView.swift | 123 ++++++++++++------ 1 file changed, 84 insertions(+), 39 deletions(-) diff --git a/LyricFever/Views/KaraokeView/KaraokeView.swift b/LyricFever/Views/KaraokeView/KaraokeView.swift index 9ef8795..f929e6d 100644 --- a/LyricFever/Views/KaraokeView/KaraokeView.swift +++ b/LyricFever/Views/KaraokeView/KaraokeView.swift @@ -9,18 +9,7 @@ import SwiftUI import SDWebImage import ColorKit import Combine - -extension Color { - var perceivedBrightness: Double { - // Convert to sRGB components - let components = self.cgColor?.components - let r = Double(components?[0] ?? 0) - let g = Double(components?[1] ?? 0) - let b = Double(components?[2] ?? 0) - - return 0.2126 * r + 0.7152 * g + 0.0722 * b - } -} +import AppKit struct VisualEffectView: NSViewRepresentable { func makeNSView(context: Context) -> NSView { @@ -47,25 +36,83 @@ struct VisualEffectView: NSViewRepresentable { } } +extension Color { + init(hex: UInt, alpha: Double = 1.0) { + self.init( + .sRGB, + red: Double((hex >> 16) & 0xFF) / 255, + green: Double((hex >> 8) & 0xFF) / 255, + blue: Double(hex & 0xFF) / 255, + opacity: alpha + ) + } + + var srgbComponents: (r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat) { + let nsColor = NSColor(self) + var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0 + nsColor.getRed(&r, green: &g, blue: &b, alpha: &a) + return (r, g, b, a) + } + + private var relativeLuminance: Double { + let (r, g, b, _) = srgbComponents + func gamma(_ c: CGFloat) -> Double { + let f = Double(c) + return f <= 0.03928 ? f / 12.92 : pow((f + 0.055) / 1.055, 2.4) + } + return 0.2126 * gamma(r) + 0.7152 * gamma(g) + 0.0722 * gamma(b) + } + + func contrastRatio(against other: Color) -> Double { + let l1 = self.relativeLuminance + let l2 = other.relativeLuminance + let lighter = max(l1, l2) + let darker = min(l1, l2) + return (lighter + 0.05) / (darker + 0.05) + } +} + struct KaraokeView: View { @Environment(ViewModel.self) var viewmodel @AppStorage("karaokeTransparency") var karaokeTransparency: Double = 50 + private var systemVisualEffectBackgroundColor: Color { + if NSApp.effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua { + return Color(hex: 0x2C2C2C) // dark mode + } else { + return Color(hex: 0xE0E0E0) // light mode + } + } + + // Blend album color over system background based on transparency + private func blendedBackgroundColor(albumColor: Color, transparency: Double) -> Color { + let systemBG = systemVisualEffectBackgroundColor + let (r1, g1, b1, _) = systemBG.srgbComponents + let (r2, g2, b2, _) = albumColor.srgbComponents + + return Color( + .sRGB, + red: Double(r1 * (1 - transparency) + r2 * transparency), + green: Double(g1 * (1 - transparency) + g2 * transparency), + blue: Double(b1 * (1 - transparency) + b2 * transparency), + opacity: 1.0 + ) + } + private var effectiveTextColor: Color { let transparency = karaokeTransparency / 100.0 - // If fully transparent, rely on system appearance if transparency <= 0.01 { return .primary } - let bgColor = viewmodel.currentAlbumArt - let brightness = bgColor.perceivedBrightness + let bg = blendedBackgroundColor(albumColor: viewmodel.currentAlbumArt, transparency: transparency) + let contrastWithWhite = Color.white.contrastRatio(against: bg) + let contrastWithBlack = Color.black.contrastRatio(against: bg) - let threshold: Double = 0.5 - return brightness > threshold ? .black : .white + return contrastWithWhite > contrastWithBlack ? .white : .black } - + func currentWords(for currentlyPlayingLyricsIndex: Int) -> String { if !viewmodel.romanizedLyrics.isEmpty { return viewmodel.romanizedLyrics[currentlyPlayingLyricsIndex] @@ -75,31 +122,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) .compositingGroup() - } - else { + } else { Text(verbatim: viewmodel.translatedLyric[currentlyPlayingLyricsIndex]) } } else { @@ -115,26 +160,26 @@ struct KaraokeView: View { Text("") } } - + @ViewBuilder var finalKaraokeView: some View { lyricsView() .lineLimit(2) - .foregroundStyle(.primary) + .foregroundStyle(effectiveTextColor) .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) + viewmodel.currentAlbumArt + .transition(.opacity) + .opacity(karaokeTransparency / 100) + } + .drawingGroup() + .background( + VisualEffectView().ignoresSafeArea() + ) + .cornerRadius(16) .onHover { hover in if viewmodel.userDefaultStorage.karaokeModeHoveringSetting { viewmodel.karaokeModeHovering = hover @@ -143,7 +188,7 @@ struct KaraokeView: View { .multilineTextAlignment(.center) .frame(minWidth: 800, maxWidth: 800, minHeight: 100, maxHeight: 100, alignment: .center) } - + var body: some View { finalKaraokeView } From a0e0cddaa6b946c889dd24ff8abf72d7fc5defd7 Mon Sep 17 00:00:00 2001 From: "Michael Y. Qiu" Date: Wed, 5 Nov 2025 17:08:02 +0800 Subject: [PATCH 06/17] Rewrite Karaoke using SwiftUI let SwiftUI manage colors --- .../Views/KaraokeView/KaraokeView.swift | 184 ++++++------------ 1 file changed, 58 insertions(+), 126 deletions(-) diff --git a/LyricFever/Views/KaraokeView/KaraokeView.swift b/LyricFever/Views/KaraokeView/KaraokeView.swift index f929e6d..bc26183 100644 --- a/LyricFever/Views/KaraokeView/KaraokeView.swift +++ b/LyricFever/Views/KaraokeView/KaraokeView.swift @@ -9,108 +9,14 @@ import SwiftUI import SDWebImage import ColorKit import Combine -import AppKit - -struct VisualEffectView: NSViewRepresentable { - func makeNSView(context: Context) -> NSView { - if #available(macOS 26.0, *) { - let glassEffectView = NSGlassEffectView() - glassEffectView.style = NSGlassEffectView.Style.regular - return glassEffectView - } else { - let visualEffectView = NSVisualEffectView() - visualEffectView.blendingMode = .behindWindow - visualEffectView.state = .active - visualEffectView.material = .hudWindow - return visualEffectView - } - } - - func updateNSView(_ nsView: NSView, context: Context) { - if #available(macOS 26.0, *), let glassEffectView = nsView as? NSGlassEffectView { - glassEffectView.style = NSGlassEffectView.Style.regular - } else if let visualEffectView = nsView as? NSVisualEffectView { - visualEffectView.material = .hudWindow - visualEffectView.blendingMode = .behindWindow - } - } -} - -extension Color { - init(hex: UInt, alpha: Double = 1.0) { - self.init( - .sRGB, - red: Double((hex >> 16) & 0xFF) / 255, - green: Double((hex >> 8) & 0xFF) / 255, - blue: Double(hex & 0xFF) / 255, - opacity: alpha - ) - } - - var srgbComponents: (r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat) { - let nsColor = NSColor(self) - var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0 - nsColor.getRed(&r, green: &g, blue: &b, alpha: &a) - return (r, g, b, a) - } - - private var relativeLuminance: Double { - let (r, g, b, _) = srgbComponents - func gamma(_ c: CGFloat) -> Double { - let f = Double(c) - return f <= 0.03928 ? f / 12.92 : pow((f + 0.055) / 1.055, 2.4) - } - return 0.2126 * gamma(r) + 0.7152 * gamma(g) + 0.0722 * gamma(b) - } - - func contrastRatio(against other: Color) -> Double { - let l1 = self.relativeLuminance - let l2 = other.relativeLuminance - let lighter = max(l1, l2) - let darker = min(l1, l2) - return (lighter + 0.05) / (darker + 0.05) - } -} struct KaraokeView: View { @Environment(ViewModel.self) var viewmodel @AppStorage("karaokeTransparency") var karaokeTransparency: Double = 50 - - private var systemVisualEffectBackgroundColor: Color { - if NSApp.effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua { - return Color(hex: 0x2C2C2C) // dark mode - } else { - return Color(hex: 0xE0E0E0) // light mode - } - } - - // Blend album color over system background based on transparency - private func blendedBackgroundColor(albumColor: Color, transparency: Double) -> Color { - let systemBG = systemVisualEffectBackgroundColor - let (r1, g1, b1, _) = systemBG.srgbComponents - let (r2, g2, b2, _) = albumColor.srgbComponents - return Color( - .sRGB, - red: Double(r1 * (1 - transparency) + r2 * transparency), - green: Double(g1 * (1 - transparency) + g2 * transparency), - blue: Double(b1 * (1 - transparency) + b2 * transparency), - opacity: 1.0 - ) - } - - private var effectiveTextColor: Color { - let transparency = karaokeTransparency / 100.0 - - if transparency <= 0.01 { - return .primary - } - - let bg = blendedBackgroundColor(albumColor: viewmodel.currentAlbumArt, transparency: transparency) - let contrastWithWhite = Color.white.contrastRatio(against: bg) - let contrastWithBlack = Color.black.contrastRatio(against: bg) - - return contrastWithWhite > contrastWithBlack ? .white : .black + // Adjust threshold here + private var useForcedWhiteText: Bool { + karaokeTransparency / 100 > 0.30 } func currentWords(for currentlyPlayingLyricsIndex: Int) -> String { @@ -161,35 +67,61 @@ struct KaraokeView: View { } } - @ViewBuilder - var finalKaraokeView: some View { - lyricsView() - .lineLimit(2) - .foregroundStyle(effectiveTextColor) - .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, *) { + 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) + } else { + // fall back + 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) + } } } From d539c8e9c4d47bc08e895f63fa09b440626663da Mon Sep 17 00:00:00 2001 From: "Michael Y. Qiu" Date: Wed, 5 Nov 2025 17:10:01 +0800 Subject: [PATCH 07/17] Add fall back to KaraokeView for OS<26.0 --- LyricFever/Views/KaraokeView/KaraokeView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/LyricFever/Views/KaraokeView/KaraokeView.swift b/LyricFever/Views/KaraokeView/KaraokeView.swift index bc26183..ef31796 100644 --- a/LyricFever/Views/KaraokeView/KaraokeView.swift +++ b/LyricFever/Views/KaraokeView/KaraokeView.swift @@ -125,3 +125,4 @@ struct KaraokeView: View { } } } + From fbbc711eb184f209c77261140c25a4159b3cc734 Mon Sep 17 00:00:00 2001 From: "Michael Y. Qiu" Date: Wed, 5 Nov 2025 23:18:28 +0800 Subject: [PATCH 08/17] Rewrite Karaoke using SwiftUI --- .../Views/KaraokeView/KaraokeView.swift | 154 ++++-------------- 1 file changed, 28 insertions(+), 126 deletions(-) diff --git a/LyricFever/Views/KaraokeView/KaraokeView.swift b/LyricFever/Views/KaraokeView/KaraokeView.swift index f929e6d..f21ada5 100644 --- a/LyricFever/Views/KaraokeView/KaraokeView.swift +++ b/LyricFever/Views/KaraokeView/KaraokeView.swift @@ -9,108 +9,14 @@ import SwiftUI import SDWebImage import ColorKit import Combine -import AppKit - -struct VisualEffectView: NSViewRepresentable { - func makeNSView(context: Context) -> NSView { - if #available(macOS 26.0, *) { - let glassEffectView = NSGlassEffectView() - glassEffectView.style = NSGlassEffectView.Style.regular - return glassEffectView - } else { - let visualEffectView = NSVisualEffectView() - visualEffectView.blendingMode = .behindWindow - visualEffectView.state = .active - visualEffectView.material = .hudWindow - return visualEffectView - } - } - - func updateNSView(_ nsView: NSView, context: Context) { - if #available(macOS 26.0, *), let glassEffectView = nsView as? NSGlassEffectView { - glassEffectView.style = NSGlassEffectView.Style.regular - } else if let visualEffectView = nsView as? NSVisualEffectView { - visualEffectView.material = .hudWindow - visualEffectView.blendingMode = .behindWindow - } - } -} - -extension Color { - init(hex: UInt, alpha: Double = 1.0) { - self.init( - .sRGB, - red: Double((hex >> 16) & 0xFF) / 255, - green: Double((hex >> 8) & 0xFF) / 255, - blue: Double(hex & 0xFF) / 255, - opacity: alpha - ) - } - - var srgbComponents: (r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat) { - let nsColor = NSColor(self) - var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0 - nsColor.getRed(&r, green: &g, blue: &b, alpha: &a) - return (r, g, b, a) - } - - private var relativeLuminance: Double { - let (r, g, b, _) = srgbComponents - func gamma(_ c: CGFloat) -> Double { - let f = Double(c) - return f <= 0.03928 ? f / 12.92 : pow((f + 0.055) / 1.055, 2.4) - } - return 0.2126 * gamma(r) + 0.7152 * gamma(g) + 0.0722 * gamma(b) - } - - func contrastRatio(against other: Color) -> Double { - let l1 = self.relativeLuminance - let l2 = other.relativeLuminance - let lighter = max(l1, l2) - let darker = min(l1, l2) - return (lighter + 0.05) / (darker + 0.05) - } -} struct KaraokeView: View { @Environment(ViewModel.self) var viewmodel @AppStorage("karaokeTransparency") var karaokeTransparency: Double = 50 - - private var systemVisualEffectBackgroundColor: Color { - if NSApp.effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua { - return Color(hex: 0x2C2C2C) // dark mode - } else { - return Color(hex: 0xE0E0E0) // light mode - } - } - - // Blend album color over system background based on transparency - private func blendedBackgroundColor(albumColor: Color, transparency: Double) -> Color { - let systemBG = systemVisualEffectBackgroundColor - let (r1, g1, b1, _) = systemBG.srgbComponents - let (r2, g2, b2, _) = albumColor.srgbComponents - - return Color( - .sRGB, - red: Double(r1 * (1 - transparency) + r2 * transparency), - green: Double(g1 * (1 - transparency) + g2 * transparency), - blue: Double(b1 * (1 - transparency) + b2 * transparency), - opacity: 1.0 - ) - } - private var effectiveTextColor: Color { - let transparency = karaokeTransparency / 100.0 - - if transparency <= 0.01 { - return .primary - } - - let bg = blendedBackgroundColor(albumColor: viewmodel.currentAlbumArt, transparency: transparency) - let contrastWithWhite = Color.white.contrastRatio(against: bg) - let contrastWithBlack = Color.black.contrastRatio(against: bg) - - return contrastWithWhite > contrastWithBlack ? .white : .black + // Adjust threshold here + private var useForcedWhiteText: Bool { + karaokeTransparency / 100 > 0.40 } func currentWords(for currentlyPlayingLyricsIndex: Int) -> String { @@ -161,35 +67,31 @@ struct KaraokeView: View { } } - @ViewBuilder - var finalKaraokeView: some View { - lyricsView() - .lineLimit(2) - .foregroundStyle(effectiveTextColor) - .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, *) { + 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) + } else { + // fall back } } From c001019c7a4550bc3e771e88ff41a7a6f02e045d Mon Sep 17 00:00:00 2001 From: "Michael Y. Qiu" Date: Wed, 5 Nov 2025 23:19:03 +0800 Subject: [PATCH 09/17] Add fallback for OS below 26 --- .../Views/KaraokeView/KaraokeView.swift | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/LyricFever/Views/KaraokeView/KaraokeView.swift b/LyricFever/Views/KaraokeView/KaraokeView.swift index f21ada5..cd947f0 100644 --- a/LyricFever/Views/KaraokeView/KaraokeView.swift +++ b/LyricFever/Views/KaraokeView/KaraokeView.swift @@ -93,5 +93,35 @@ struct KaraokeView: View { .frame(minWidth: 800, maxWidth: 800, minHeight: 100, maxHeight: 100, alignment: .center) } else { // fall back + 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) + } } } From 6491d023a8ffeed448e72bed82a2acadf78ecc72 Mon Sep 17 00:00:00 2001 From: "Michael Y. Qiu" Date: Wed, 5 Nov 2025 23:19:44 +0800 Subject: [PATCH 10/17] Change forcedWhiteText threshold to 0.35 --- LyricFever/Views/KaraokeView/KaraokeView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LyricFever/Views/KaraokeView/KaraokeView.swift b/LyricFever/Views/KaraokeView/KaraokeView.swift index cd947f0..31bd5b1 100644 --- a/LyricFever/Views/KaraokeView/KaraokeView.swift +++ b/LyricFever/Views/KaraokeView/KaraokeView.swift @@ -16,7 +16,7 @@ struct KaraokeView: View { // Adjust threshold here private var useForcedWhiteText: Bool { - karaokeTransparency / 100 > 0.40 + karaokeTransparency / 100 >= 0.35 } func currentWords(for currentlyPlayingLyricsIndex: Int) -> String { From 4be02e79f8650445c292018b5cfc4fb81dac7fb7 Mon Sep 17 00:00:00 2001 From: "Michael Y. Qiu" Date: Sun, 8 Mar 2026 22:03:02 +0800 Subject: [PATCH 11/17] Fix wrapping in ViewModel --- Localizable.xcstrings | 3 +++ LyricFever/ViewModel.swift | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index b604e7b..ce44718 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -336,6 +336,7 @@ } }, "Arist Name" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -1442,6 +1443,7 @@ } }, "More options" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -1464,6 +1466,7 @@ } }, "More options, AirPlay delay enabled" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { diff --git a/LyricFever/ViewModel.swift b/LyricFever/ViewModel.swift index 3fa43b9..5afa9d2 100644 --- a/LyricFever/ViewModel.swift +++ b/LyricFever/ViewModel.swift @@ -55,7 +55,7 @@ import MediaRemoteAdapter // } else { // self.appleMusicUniqueIdentifier = data.payload.uniqueIdentifier // } - guard let artwork = data.payload.artwork else { + guard let artwork = data?.payload.artwork else { if self.currentlyPlaying == nil { self.artworkImage = nil } From a1712ecc75b307818a6e577714055a62d9d64da8 Mon Sep 17 00:00:00 2001 From: "Michael Y. Qiu" Date: Sun, 8 Mar 2026 22:20:58 +0800 Subject: [PATCH 12/17] Fix fallback failure because of Spotify catch fail --- Localizable.xcstrings | 9 ++++ .../Support Files/UserDefaultStorage.swift | 1 + LyricFever/ViewModel.swift | 50 +++++++++++++++---- .../OnboardingView/MainSettingsView.swift | 8 +++ 4 files changed, 59 insertions(+), 9 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index ce44718..ee25a80 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -1215,6 +1215,9 @@ } } } + }, + "LRCLIB" : { + }, "Lyric Fever: Fullscreen" : { "localizations" : { @@ -1487,6 +1490,9 @@ } } } + }, + "NetEase" : { + }, "Next" : { "localizations" : { @@ -2047,6 +2053,9 @@ } } } + }, + "Primary Lyric Source" : { + }, "Quit" : { "localizations" : { 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 5afa9d2..9473c2e 100644 --- a/LyricFever/ViewModel.swift +++ b/LyricFever/ViewModel.swift @@ -279,10 +279,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 @@ -1141,14 +1163,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/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") From 23e856ecdfaa8498a66f10160e7f6452beeb1a22 Mon Sep 17 00:00:00 2001 From: "Michael Y. Qiu" Date: Sun, 8 Mar 2026 22:36:07 +0800 Subject: [PATCH 13/17] Fix app update aborting lyric rendering --- LyricFever/ViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LyricFever/ViewModel.swift b/LyricFever/ViewModel.swift index 9473c2e..836d2de 100644 --- a/LyricFever/ViewModel.swift +++ b/LyricFever/ViewModel.swift @@ -807,7 +807,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 From df5548c1c502d90d05d09fc2801ffbb820feb69d Mon Sep 17 00:00:00 2001 From: "Michael Y. Qiu" Date: Sun, 8 Mar 2026 22:46:39 +0800 Subject: [PATCH 14/17] Osscilate lyric view to ensure Liquid Glass is lively --- LyricFever/ViewModel.swift | 2 +- .../Views/KaraokeView/KaraokeView.swift | 45 ++++++++++--------- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/LyricFever/ViewModel.swift b/LyricFever/ViewModel.swift index 836d2de..1626bac 100644 --- a/LyricFever/ViewModel.swift +++ b/LyricFever/ViewModel.swift @@ -53,7 +53,7 @@ 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 { if self.currentlyPlaying == nil { diff --git a/LyricFever/Views/KaraokeView/KaraokeView.swift b/LyricFever/Views/KaraokeView/KaraokeView.swift index 51b3509..da0460a 100644 --- a/LyricFever/Views/KaraokeView/KaraokeView.swift +++ b/LyricFever/Views/KaraokeView/KaraokeView.swift @@ -69,28 +69,33 @@ struct KaraokeView: View { var body: some View { if #available(macOS 26.0, *) { - 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) + // osscilate the lyrics view so the Liquid Glass rendering is always lively + TimelineView(.animation) { timeline in + let offset = 0.1 * sin(timeline.date.timeIntervalSinceReferenceDate * .pi) + 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 + .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) + .multilineTextAlignment(.center) + .frame(minWidth: 800, maxWidth: 800, minHeight: 100, maxHeight: 100, alignment: .center) + .offset(y: offset) + } } else { // fall back lyricsView() From 6c8d71ab26b3c08f49b131f1aa2647237e26ddcc Mon Sep 17 00:00:00 2001 From: "Michael Y. Qiu" Date: Sun, 8 Mar 2026 22:49:23 +0800 Subject: [PATCH 15/17] Hide Karaoke when lyric is blank --- LyricFever/ViewModel.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/LyricFever/ViewModel.swift b/LyricFever/ViewModel.swift index 1626bac..157a1d1 100644 --- a/LyricFever/ViewModel.swift +++ b/LyricFever/ViewModel.swift @@ -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 From 9a8104e4755d86a59d27a56456f3385b5a34795f Mon Sep 17 00:00:00 2001 From: "Michael Y. Qiu" Date: Sun, 8 Mar 2026 22:55:23 +0800 Subject: [PATCH 16/17] Increase the offset frequency --- LyricFever/Views/KaraokeView/KaraokeView.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/LyricFever/Views/KaraokeView/KaraokeView.swift b/LyricFever/Views/KaraokeView/KaraokeView.swift index da0460a..d5dbf89 100644 --- a/LyricFever/Views/KaraokeView/KaraokeView.swift +++ b/LyricFever/Views/KaraokeView/KaraokeView.swift @@ -71,7 +71,7 @@ struct KaraokeView: View { 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 * .pi) + 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) @@ -97,7 +97,6 @@ struct KaraokeView: View { .offset(y: offset) } } else { - // fall back lyricsView() .lineLimit(2) .foregroundStyle(useForcedWhiteText ? Color.white : Color.primary) From 83268d846f7439a1f66de87aec6e9859c6f30cc4 Mon Sep 17 00:00:00 2001 From: "Michael Y. Qiu" Date: Sun, 8 Mar 2026 23:07:01 +0800 Subject: [PATCH 17/17] Update localization --- Localizable.xcstrings | 52 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index ee25a80..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" : { @@ -1217,7 +1219,7 @@ } }, "LRCLIB" : { - + "shouldTranslate" : false }, "Lyric Fever: Fullscreen" : { "localizations" : { @@ -1492,7 +1494,26 @@ } }, "NetEase" : { - + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "网易" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "網易" + } + }, + "zh-Hant-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "網易" + } + } + } }, "Next" : { "localizations" : { @@ -2055,7 +2076,26 @@ } }, "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" : {