Fix apps issues and bump to v1.1

Fixes:

- Fix interface text and glitches when switching themes.

- Redesign settings on application.

- Optimize code and compilation warnings.
This commit is contained in:
Jesus Chapman 2026-06-20 17:12:06 -05:00
parent 32d4dd3501
commit 957a4ab7e7
13 changed files with 2425 additions and 1982 deletions

View File

@ -267,7 +267,7 @@
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 001;
CURRENT_PROJECT_VERSION = 002;
DEVELOPMENT_TEAM = 7CQPG22234;
ENABLE_APP_SANDBOX = NO;
ENABLE_FILE_ACCESS_MUSIC_FOLDER = readwrite;
@ -291,7 +291,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 1.0;
MARKETING_VERSION = 1.1;
PRODUCT_BUNDLE_IDENTIFIER = dev.jesuschapman.Valentine;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
@ -312,7 +312,7 @@
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 001;
CURRENT_PROJECT_VERSION = 002;
DEVELOPMENT_TEAM = 7CQPG22234;
ENABLE_APP_SANDBOX = NO;
ENABLE_FILE_ACCESS_MUSIC_FOLDER = readwrite;
@ -336,7 +336,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 1.0;
MARKETING_VERSION = 1.1;
PRODUCT_BUNDLE_IDENTIFIER = dev.jesuschapman.Valentine;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;

View File

@ -14,55 +14,48 @@ struct ContentView: View {
@State private var isTargeted = false
@State private var isPlaylistVisible = true
@State private var wasWide = true
@State private var windowSize: CGSize? = nil
@AppStorage("lastNormalWidth") private var lastNormalWidth: Double = 900
@AppStorage("lastNormalHeight") private var lastNormalHeight: Double = 600
var body: some View {
GeometryReader { geometry in
let isWide = geometry.size.width > 600
let currentWidth = geometry.size.width
let isWide = currentWidth > 600
Group {
if engine.queue.isEmpty {
emptyStateView
} else {
HStack(spacing: 0) {
if isWide {
if isPlaylistVisible {
PlaylistView(engine: engine)
.frame(width: 280)
.background(Color.black.opacity(0.2))
.transition(.move(edge: .leading))
}
if isPlaylistVisible {
PlaylistView(engine: engine)
.frame(minWidth: isWide ? 280 : nil,
idealWidth: isWide ? 280 : nil,
maxWidth: isWide ? 280 : .infinity,
maxHeight: .infinity)
.background(Color.black.opacity(0.2))
.transition(.asymmetric(insertion: .move(edge: .leading), removal: .move(edge: .leading)))
}
if isWide || !isPlaylistVisible {
PlayerView(
engine: engine,
togglePlaylist: {},
togglePlaylist: {
withAnimation(.spring()) {
isPlaylistVisible.toggle()
}
},
isPlaylistVisible: isPlaylistVisible,
showToggle: false
showToggle: !isWide
)
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else {
if isPlaylistVisible {
PlaylistView(engine: engine)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.black.opacity(0.2))
.transition(.asymmetric(insertion: .move(edge: .leading), removal: .move(edge: .leading)))
} else {
PlayerView(
engine: engine,
togglePlaylist: {},
isPlaylistVisible: isPlaylistVisible,
showToggle: false
)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.transition(.asymmetric(insertion: .move(edge: .trailing), removal: .move(edge: .trailing)))
}
.transition(.asymmetric(insertion: .move(edge: .trailing), removal: .move(edge: .trailing)))
}
}
}
}
.frame(width: geometry.size.width, height: geometry.size.height)
.background(backgroundLayer)
.toolbar {
ToolbarItem(placement: .navigation) {
@ -76,7 +69,11 @@ struct ContentView: View {
}
}
.toolbarBackground(.hidden, for: .windowToolbar)
.onChange(of: geometry.size) { newSize in
.onDrop(of: ["public.file-url"], isTargeted: $isTargeted) { providers in
handleDrop(providers: providers)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.onChange(of: geometry.size) { _, newSize in
lastNormalWidth = Double(newSize.width)
lastNormalHeight = Double(newSize.height)
@ -91,10 +88,7 @@ struct ContentView: View {
}
}
}
.onDrop(of: ["public.file-url"], isTargeted: $isTargeted) { providers in
handleDrop(providers: providers)
}
.frame(minWidth: 400, maxWidth: .infinity, minHeight: 540, maxHeight: .infinity)
.frame(minWidth: 400, minHeight: 540)
}
private var backgroundLayer: some View {
@ -105,17 +99,24 @@ struct ContentView: View {
art
.resizable()
.aspectRatio(contentMode: .fill)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
.clipped()
.blur(radius: 80)
.opacity(0.7)
.ignoresSafeArea()
.animation(.easeInOut(duration: 1.5), value: engine.currentTrack?.id)
} else {
LinearGradient(
colors: [Color(red: 0.2, green: 0.1, blue: 0.15), Color(red: 0.1, green: 0.1, blue: 0.1)],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
.ignoresSafeArea()
if colorScheme == .dark {
LinearGradient(
colors: [Color(red: 0.2, green: 0.1, blue: 0.15), Color(red: 0.1, green: 0.1, blue: 0.1)],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
.ignoresSafeArea()
} else {
Color(NSColor.windowBackgroundColor)
.ignoresSafeArea()
}
}
}
}
@ -232,3 +233,11 @@ struct HoverZoomButton: View {
.buttonStyle(.plain)
}
}
struct WindowSizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = nextValue()
}
}

View File

@ -7,14 +7,21 @@
import SwiftUI
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ notification: Notification) {
NSWindow.allowsAutomaticWindowTabbing = false
}
}
@main
struct ValentineApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
RootView()
}
.windowStyle(.hiddenTitleBar)
.windowResizability(.contentSize)
.windowResizability(.contentMinSize)
.commands {
ValentineCommands()
}
@ -26,12 +33,11 @@ struct ValentineApp: App {
.windowResizability(.contentSize)
.defaultSize(width: 650, height: 480)
Window("Modify Appearance", id: "lyricsAppearance") {
LyricsAppearanceView()
Window("Settings", id: "settings") {
SettingsView()
}
.windowStyle(.hiddenTitleBar)
.windowResizability(.contentSize)
.defaultSize(width: 400, height: 600)
.windowResizability(.contentMinSize)
}
}
@ -52,12 +58,15 @@ struct RootView: View {
.environmentObject(engine)
}
}
.preferredColorScheme(appTheme == 1 ? .light : (appTheme == 2 ? .dark : nil))
.animation(.easeInOut, value: isMiniPlayerMode)
.onAppear {
updateTheme(theme: appTheme)
configureWindow(forMiniPlayer: isMiniPlayerMode)
}
.onChange(of: isMiniPlayerMode) { newValue in
.onChange(of: appTheme) { _, newTheme in
updateTheme(theme: newTheme)
}
.onChange(of: isMiniPlayerMode) { _, newValue in
configureWindow(forMiniPlayer: newValue)
}
.sheet(isPresented: $engine.showLyricsEditor) {
@ -76,11 +85,28 @@ struct RootView: View {
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("ReinstallMutagen"))) { _ in engine.showMutagenInstaller = true }
}
private func updateTheme(theme: Int) {
#if os(macOS)
DispatchQueue.main.async {
let appearance: NSAppearance?
switch theme {
case 1: appearance = NSAppearance(named: .aqua)
case 2: appearance = NSAppearance(named: .darkAqua)
default: appearance = nil
}
NSApplication.shared.appearance = appearance
}
#endif
}
private func configureWindow(forMiniPlayer: Bool) {
#if os(macOS)
DispatchQueue.main.async {
for window in NSApplication.shared.windows {
if window.className == "NSWindow" || window.className.contains("SwiftUI") {
let id = window.identifier?.rawValue ?? ""
if id.contains("settings") || id.contains("about") { continue }
window.level = forMiniPlayer ? .floating : .normal
window.standardWindowButton(.closeButton)?.isHidden = forMiniPlayer
window.standardWindowButton(.miniaturizeButton)?.isHidden = forMiniPlayer
@ -119,11 +145,7 @@ struct RootView: View {
}
struct ValentineCommands: Commands {
@AppStorage("isGlowEffectEnabled") private var isGlowEffectEnabled = false
@AppStorage("isNeonEffectEnabled") private var isNeonEffectEnabled = false
@AppStorage("isMiniPlayerMode") private var isMiniPlayerMode = false
@AppStorage("miniPlayerGlassMode") private var miniPlayerGlassMode = 0
@AppStorage("appTheme") private var appTheme = 0
@Environment(\.openWindow) var openWindow
var body: some Commands {
@ -135,6 +157,15 @@ struct ValentineCommands: Commands {
}
}
CommandGroup(replacing: .appSettings) {
Button(action: { openWindow(id: "settings") }) {
Label("Settings...", systemImage: "gearshape")
}
.keyboardShortcut(",", modifiers: [.command])
}
CommandGroup(replacing: .newItem) {
Button(action: { NotificationCenter.default.post(name: NSNotification.Name("AddFile"), object: nil) }) {
Label("Add File...", systemImage: "doc.badge.plus")
@ -160,45 +191,15 @@ struct ValentineCommands: Commands {
Label("Edit Lyrics", systemImage: "music.note.list")
}
.keyboardShortcut("e", modifiers: [.command])
Button(action: { openWindow(id: "lyricsAppearance") }) {
Label("Modify Appearance", systemImage: "textformat.alt")
}
}
CommandGroup(replacing: .help) {
Button(action: { NotificationCenter.default.post(name: NSNotification.Name("ReinstallMutagen"), object: nil) }) {
Label("Reinstall Mutagen", systemImage: "arrow.triangle.2.circlepath")
}
}
CommandGroup(after: .toolbar) {
Divider()
Menu {
Toggle("Glow Effect", isOn: $isGlowEffectEnabled)
Toggle("Neon Effect", isOn: $isNeonEffectEnabled)
} label: {
Label("Synced Lyrics Settings", systemImage: "sparkles")
}
Menu {
Picker("Mode", selection: $miniPlayerGlassMode) {
Text("Tinted (System Theme)").tag(0)
Text("Transparent (No Tint)").tag(1)
}
.pickerStyle(InlinePickerStyle())
} label: {
Label("Mini Player Background", systemImage: "macwindow")
}
Menu {
Picker("Theme", selection: $appTheme) {
Text("Follow System").tag(0)
Text("Light").tag(1)
Text("Dark").tag(2)
}
.pickerStyle(InlinePickerStyle())
} label: {
Label("App Theme", systemImage: "paintpalette")
}
}
CommandGroup(after: .windowList) {
Button(action: { isMiniPlayerMode.toggle() }) {
Label(isMiniPlayerMode ? "Switch to Full Player" : "Switch to Mini-Player", systemImage: isMiniPlayerMode ? "arrow.up.left.and.arrow.down.right" : "pip.enter")

File diff suppressed because it is too large Load Diff

View File

@ -32,7 +32,7 @@ struct Track: Identifiable, Hashable {
}
mutating func loadMetadata() async {
let asset = AVAsset(url: url)
let asset = AVURLAsset(url: url)
do {
self.duration = try await asset.load(.duration).seconds

View File

@ -56,11 +56,13 @@ class AudioEngine: ObservableObject {
setupRemoteCommandCenter()
NotificationCenter.default.addObserver(forName: UserDefaults.didChangeNotification, object: nil, queue: .main) { [weak self] _ in
guard let self = self else { return }
let newGlow = UserDefaults.standard.bool(forKey: "isGlowEffectEnabled")
let newNeon = UserDefaults.standard.bool(forKey: "isNeonEffectEnabled")
if self.isGlowEffectEnabled != newGlow { self.isGlowEffectEnabled = newGlow }
if self.isNeonEffectEnabled != newNeon { self.isNeonEffectEnabled = newNeon }
Task { @MainActor in
guard let self = self else { return }
let newGlow = UserDefaults.standard.bool(forKey: "isGlowEffectEnabled")
let newNeon = UserDefaults.standard.bool(forKey: "isNeonEffectEnabled")
if self.isGlowEffectEnabled != newGlow { self.isGlowEffectEnabled = newGlow }
if self.isNeonEffectEnabled != newNeon { self.isNeonEffectEnabled = newNeon }
}
}
}
@ -170,7 +172,7 @@ class AudioEngine: ObservableObject {
if fileManager.fileExists(atPath: url.path, isDirectory: &isDirectory) {
if isDirectory.boolValue {
if let enumerator = fileManager.enumerator(at: url, includingPropertiesForKeys: [.isRegularFileKey], options: [.skipsHiddenFiles, .skipsPackageDescendants]) {
for case let fileURL as URL in enumerator {
while let fileURL = enumerator.nextObject() as? URL {
let ext = fileURL.pathExtension.lowercased()
if supportedExtensions.contains(ext) {
audioURLs.append(fileURL)
@ -212,11 +214,13 @@ class AudioEngine: ObservableObject {
let interval = CMTime(seconds: 0.1, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
timeObserver = player?.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] time in
guard let self = self else { return }
self.currentTime = time.seconds
if self.duration > 0 && self.currentTime >= self.duration - 0.1 {
self.nextTrack(isAutomatic: true)
Task { @MainActor in
guard let self = self else { return }
self.currentTime = time.seconds
if self.duration > 0 && self.currentTime >= self.duration - 0.1 {
self.nextTrack(isAutomatic: true)
}
}
}

View File

@ -1,142 +0,0 @@
import SwiftUI
struct LyricsAppearanceView: View {
@ObservedObject var settings = LyricsAppearanceManager.shared
@State private var previewIsDark = true
@State private var previewNeon = false
@State private var previewGlow = false
@State private var applyMode = 0 // 0: Both Themes, 1: Specific Theme
// Bindings for the currently selected theme to edit
private var isEditingDark: Bool {
if applyMode == 0 { return previewIsDark }
return previewIsDark // Wait, if specific theme, it edits the preview's current theme.
}
private var fontDesignBinding: Binding<Int> {
Binding(
get: { isEditingDark ? settings.fontDesignDark : settings.fontDesignLight },
set: { newValue in
if applyMode == 0 {
settings.fontDesignDark = newValue
settings.fontDesignLight = newValue
} else {
if isEditingDark { settings.fontDesignDark = newValue }
else { settings.fontDesignLight = newValue }
}
}
)
}
private func colorBinding(for stringBindingLight: Binding<String>, stringBindingDark: Binding<String>, defaultColor: Color) -> Binding<Color> {
Binding<Color>(
get: {
let hex = isEditingDark ? stringBindingDark.wrappedValue : stringBindingLight.wrappedValue
return hex.isEmpty ? defaultColor : Color(hex: hex)
},
set: { newValue in
let hex = newValue.toHex()
if applyMode == 0 {
stringBindingDark.wrappedValue = hex
stringBindingLight.wrappedValue = hex
} else {
if isEditingDark { stringBindingDark.wrappedValue = hex }
else { stringBindingLight.wrappedValue = hex }
}
}
)
}
var body: some View {
VStack(spacing: 0) {
// PREVIEW SECTION
VStack {
HStack {
Text("Preview")
.font(.headline)
.foregroundColor(.primary)
Spacer()
Toggle("Dark Mode", isOn: $previewIsDark)
.toggleStyle(.switch)
.controlSize(.mini)
}
.padding()
ZStack {
RoundedRectangle(cornerRadius: 12, style: .continuous)
.fill(previewIsDark ? Color(white: 0.1) : Color(white: 0.95))
VStack(spacing: 16) {
previewText("Lorem ipsum dolor sit amet", isActive: false)
previewText("Consectetur adipiscing elit", isActive: true)
previewText("Sed do eiusmod tempor", isActive: false)
}
}
.frame(height: 180)
.padding(.horizontal)
.padding(.bottom)
}
.background(Color(NSColor.windowBackgroundColor))
Divider()
// SETTINGS SECTION
ScrollView {
VStack(spacing: 24) {
Picker("Apply to:", selection: $applyMode) {
Text("Both Themes").tag(0)
Text(previewIsDark ? "Dark Theme Only" : "Light Theme Only").tag(1)
}
.pickerStyle(SegmentedPickerStyle())
HStack {
Toggle("Neon Effect", isOn: $previewNeon)
Spacer()
Toggle("Glow Effect", isOn: $previewGlow)
}
VStack(alignment: .leading, spacing: 12) {
Text("Typography")
.font(.subheadline)
.fontWeight(.semibold)
Picker("Font Design", selection: fontDesignBinding) {
Text("Rounded").tag(1)
Text("Default").tag(0)
Text("Monospaced").tag(2)
Text("Serif").tag(3)
}
}
VStack(alignment: .leading, spacing: 12) {
Text("Colors")
.font(.subheadline)
.fontWeight(.semibold)
ColorPicker("Font Color", selection: colorBinding(for: $settings.fontColorLight, stringBindingDark: $settings.fontColorDark, defaultColor: .primary))
ColorPicker("Neon Color", selection: colorBinding(for: $settings.neonColorLight, stringBindingDark: $settings.neonColorDark, defaultColor: .white))
ColorPicker("Glow Color", selection: colorBinding(for: $settings.glowColorLight, stringBindingDark: $settings.glowColorDark, defaultColor: .accentColor))
}
Button("Reset to Defaults") {
settings.resetToDefaults()
}
.padding(.top, 10)
}
.padding(24)
}
}
.frame(width: 400, height: 600)
}
private func previewText(_ text: String, isActive: Bool) -> some View {
Text(text)
.font(.system(size: isActive ? 24 : 18, weight: isActive ? .bold : .medium, design: settings.getFontDesign(isDark: previewIsDark)))
.foregroundColor((previewNeon && isActive) ? settings.getNeonColor(isDark: previewIsDark) : settings.getFontColor(isDark: previewIsDark, isActive: isActive))
.shadow(color: (previewNeon && isActive) ? settings.getNeonColor(isDark: previewIsDark).opacity(0.8) : .clear, radius: 10, x: 0, y: 0)
.shadow(color: (previewNeon && isActive) ? settings.getNeonColor(isDark: previewIsDark).opacity(0.4) : .clear, radius: 20, x: 0, y: 0)
.shadow(color: (previewGlow && isActive) ? settings.getGlowColor(isDark: previewIsDark).opacity(0.8) : .clear, radius: 15, x: 0, y: 0)
.shadow(color: (previewGlow && isActive) ? settings.getGlowColor(isDark: previewIsDark).opacity(0.5) : .clear, radius: 5, x: 0, y: 0)
}
}

View File

@ -59,14 +59,14 @@ struct LyricsView: View {
endPoint: .bottom
)
)
.onChange(of: activeLineIndex) { newIndex in
.onChange(of: activeLineIndex) { _, newIndex in
if let index = newIndex {
withAnimation(.spring(response: 0.6, dampingFraction: 0.8, blendDuration: 0)) {
proxy.scrollTo(index, anchor: .center)
}
}
}
.onChange(of: geo.size) { _ in
.onChange(of: geo.size) { _, _ in
if let index = activeLineIndex {
proxy.scrollTo(index, anchor: .center)
}

View File

@ -8,7 +8,7 @@ struct PlayerView: View {
var body: some View {
VStack(spacing: 8) {
Spacer(minLength: 8)
Spacer(minLength: 0)
if engine.showLyrics {
LyricsView(engine: engine)
@ -23,6 +23,8 @@ struct PlayerView: View {
art
.resizable()
.aspectRatio(contentMode: .fill)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.clipped()
)
.clipShape(RoundedRectangle(cornerRadius: 16))
.shadow(color: .black.opacity(0.4), radius: 15, x: 0, y: 10)
@ -40,8 +42,7 @@ struct PlayerView: View {
.layoutPriority(1)
}
Spacer(minLength: 16)
Spacer(minLength: 0)
WaveformView(engine: engine)
.frame(height: 50)
.padding(.horizontal, 32)
@ -70,19 +71,19 @@ struct PlayerView: View {
.padding(.horizontal, 16)
.layoutPriority(1)
Spacer(minLength: 8)
Spacer(minLength: 0)
PlaybackControlsView(engine: engine)
.layoutPriority(2)
Spacer(minLength: 16)
Spacer(minLength: 0)
VolumeControlView(engine: engine)
.frame(maxWidth: .infinity)
.padding(.horizontal, 32)
.layoutPriority(2)
Spacer(minLength: 16)
Spacer(minLength: 0)
HStack(spacing: 24) {
Button(action: {
@ -153,7 +154,7 @@ struct PlayerView: View {
.padding(.bottom, 12)
.layoutPriority(2)
Spacer(minLength: 12)
Spacer(minLength: 0)
}
.safeAreaPadding(.top, 24)
.safeAreaPadding(.bottom, 16)

View File

@ -199,6 +199,7 @@ struct CreditSection: View {
ForEach(0..<items.count, id: \.self) { index in
Text(items[index])
.font(.system(size: 13))
.fixedSize(horizontal: false, vertical: true)
}
}
}

View File

@ -0,0 +1,449 @@
import SwiftUI
enum SettingsTab: String, CaseIterable, Identifiable {
case general = "General"
case lyrics = "Lyrics"
var id: String { self.rawValue }
var icon: String {
switch self {
case .general: return "gear"
case .lyrics: return "textformat.alt"
}
}
var color: Color {
switch self {
case .general: return .gray
case .lyrics: return .blue
}
}
}
struct SettingsView: View {
@Environment(\.dismiss) var dismiss
@State private var selectedTab: SettingsTab? = .general
@AppStorage("appTheme") private var appTheme = 0
var body: some View {
NavigationSplitView {
List(selection: $selectedTab) {
ForEach(SettingsTab.allCases) { tab in
NavigationLink(value: tab) {
Label {
Text(LocalizedStringKey(tab.rawValue))
} icon: {
Image(systemName: tab.icon)
.foregroundColor(.white)
.frame(width: 20, height: 20)
.background(tab.color, in: RoundedRectangle(cornerRadius: 6))
}
}
}
}
.navigationTitle("Settings")
.listStyle(.sidebar)
} detail: {
switch selectedTab {
case .general:
GeneralSettingsView()
.navigationTitle(LocalizedStringKey(SettingsTab.general.rawValue))
case .lyrics:
LyricsAppearanceView()
.navigationTitle(LocalizedStringKey(SettingsTab.lyrics.rawValue))
case .none:
Text("Select a setting")
}
}
.frame(minWidth: 600, idealWidth: 700, minHeight: 400, idealHeight: 500)
.background(WindowButtonsHider())
.toolbar {
ToolbarItem(placement: .navigation) {
Button(action: { dismiss() }) {
Image(systemName: "xmark")
}
.help("Close Settings")
}
}
}
}
struct GeneralSettingsView: View {
@AppStorage("isGlowEffectEnabled") private var isGlowEffectEnabled = false
@AppStorage("isNeonEffectEnabled") private var isNeonEffectEnabled = false
@AppStorage("miniPlayerGlassMode") private var miniPlayerGlassMode = 0
@AppStorage("appTheme") private var appTheme = 0
var body: some View {
Form {
Section(header: Text("Appearance")) {
VStack(alignment: .leading, spacing: 12) {
Text("App Theme")
ThemeSelectionView(selection: $appTheme)
}
VStack(alignment: .leading, spacing: 4) {
Text("Mini Player Appearance")
.font(.body)
Text("Choose an appearance for the mini player.")
.font(.caption)
.foregroundColor(.secondary)
LiquidGlassSelectionView(selection: $miniPlayerGlassMode)
.padding(.top, 4)
}
.padding(.vertical, 8)
}
Section(header: Text("Synced Lyrics Effects")) {
Toggle("Glow Effect", isOn: $isGlowEffectEnabled)
Toggle("Neon Effect", isOn: $isNeonEffectEnabled)
}
}
.formStyle(.grouped)
}
}
struct LyricsAppearanceView: View {
@ObservedObject var settings = LyricsAppearanceManager.shared
@State private var previewIsDark = true
@State private var previewNeon = false
@State private var previewGlow = false
@State private var applyMode = 0 // 0: Both Themes, 1: Specific Theme
// Bindings for the currently selected theme to edit
private var isEditingDark: Bool {
if applyMode == 0 { return previewIsDark }
return previewIsDark // Wait, if specific theme, it edits the preview's current theme.
}
private var fontDesignBinding: Binding<Int> {
Binding(
get: { isEditingDark ? settings.fontDesignDark : settings.fontDesignLight },
set: { newValue in
if applyMode == 0 {
settings.fontDesignDark = newValue
settings.fontDesignLight = newValue
} else {
if isEditingDark { settings.fontDesignDark = newValue }
else { settings.fontDesignLight = newValue }
}
}
)
}
private func colorBinding(for stringBindingLight: Binding<String>, stringBindingDark: Binding<String>, defaultColor: Color) -> Binding<Color> {
Binding<Color>(
get: {
let hex = isEditingDark ? stringBindingDark.wrappedValue : stringBindingLight.wrappedValue
return hex.isEmpty ? defaultColor : Color(hex: hex)
},
set: { newValue in
let hex = newValue.toHex()
if applyMode == 0 {
stringBindingDark.wrappedValue = hex
stringBindingLight.wrappedValue = hex
} else {
if isEditingDark { stringBindingDark.wrappedValue = hex }
else { stringBindingLight.wrappedValue = hex }
}
}
)
}
var body: some View {
VStack(spacing: 0) {
// PREVIEW SECTION
VStack {
HStack {
Text("Preview")
.font(.headline)
.foregroundColor(.primary)
Spacer()
Toggle("Dark Mode", isOn: $previewIsDark)
.toggleStyle(.switch)
.controlSize(.mini)
}
.padding()
ZStack {
RoundedRectangle(cornerRadius: 12, style: .continuous)
.fill(previewIsDark ? Color(white: 0.1) : Color(white: 0.95))
VStack(spacing: 16) {
previewText("Lorem ipsum dolor sit amet", isActive: false)
previewText("Consectetur adipiscing elit", isActive: true)
previewText("Sed do eiusmod tempor", isActive: false)
}
}
.frame(height: 180)
.padding(.horizontal)
.padding(.bottom)
}
.background(Color(NSColor.windowBackgroundColor))
Divider()
// SETTINGS SECTION
ScrollView {
VStack(spacing: 24) {
Picker("Apply to:", selection: $applyMode) {
Text("Both Themes").tag(0)
Text(previewIsDark ? "Dark Theme Only" : "Light Theme Only").tag(1)
}
.pickerStyle(SegmentedPickerStyle())
HStack {
Toggle("Neon Effect", isOn: $previewNeon)
Spacer()
Toggle("Glow Effect", isOn: $previewGlow)
}
VStack(alignment: .leading, spacing: 12) {
Text("Typography")
.font(.subheadline)
.fontWeight(.semibold)
Picker("Font Design", selection: fontDesignBinding) {
Text("Rounded").tag(1)
Text("Default").tag(0)
Text("Monospaced").tag(2)
Text("Serif").tag(3)
}
}
VStack(alignment: .leading, spacing: 12) {
Text("Colors")
.font(.subheadline)
.fontWeight(.semibold)
ColorPicker("Font Color", selection: colorBinding(for: $settings.fontColorLight, stringBindingDark: $settings.fontColorDark, defaultColor: .primary))
ColorPicker("Neon Color", selection: colorBinding(for: $settings.neonColorLight, stringBindingDark: $settings.neonColorDark, defaultColor: .white))
ColorPicker("Glow Color", selection: colorBinding(for: $settings.glowColorLight, stringBindingDark: $settings.glowColorDark, defaultColor: .accentColor))
}
Button("Reset to Defaults") {
settings.resetToDefaults()
}
.padding(.top, 10)
}
.padding(24)
}
}
}
private func previewText(_ text: String, isActive: Bool) -> some View {
Text(text)
.font(.system(size: isActive ? 24 : 18, weight: isActive ? .bold : .medium, design: settings.getFontDesign(isDark: previewIsDark)))
.foregroundColor((previewNeon && isActive) ? settings.getNeonColor(isDark: previewIsDark) : settings.getFontColor(isDark: previewIsDark, isActive: isActive))
.shadow(color: (previewNeon && isActive) ? settings.getNeonColor(isDark: previewIsDark).opacity(0.8) : .clear, radius: 10, x: 0, y: 0)
.shadow(color: (previewNeon && isActive) ? settings.getNeonColor(isDark: previewIsDark).opacity(0.4) : .clear, radius: 20, x: 0, y: 0)
.shadow(color: (previewGlow && isActive) ? settings.getGlowColor(isDark: previewIsDark).opacity(0.8) : .clear, radius: 15, x: 0, y: 0)
.shadow(color: (previewGlow && isActive) ? settings.getGlowColor(isDark: previewIsDark).opacity(0.5) : .clear, radius: 5, x: 0, y: 0)
}
}
enum ThemeStyle {
case system, light, dark
}
struct ThemeSelectionView: View {
@Binding var selection: Int
var body: some View {
HStack(spacing: 20) {
ThemeThumbnail(title: "Follow System", isSelected: selection == 0, style: .system) {
selection = 0
}
ThemeThumbnail(title: "Light", isSelected: selection == 1, style: .light) {
selection = 1
}
ThemeThumbnail(title: "Dark", isSelected: selection == 2, style: .dark) {
selection = 2
}
}
.padding(.vertical, 8)
}
}
struct ThemeThumbnail: View {
let title: LocalizedStringKey
let isSelected: Bool
let style: ThemeStyle
let action: () -> Void
var body: some View {
VStack(spacing: 8) {
Button(action: action) {
ZStack {
// Background
RoundedRectangle(cornerRadius: 8)
.fill(Color.clear)
.frame(width: 76, height: 52)
if style == .system {
HStack(spacing: 0) {
ZStack {
LinearGradient(colors: [.cyan.opacity(0.6), .white], startPoint: .topLeading, endPoint: .bottomTrailing)
WindowGraphic(isDark: false)
}
ZStack {
LinearGradient(colors: [.blue.opacity(0.8), .black], startPoint: .topLeading, endPoint: .bottomTrailing)
WindowGraphic(isDark: true)
}
}
.frame(width: 72, height: 48)
.clipShape(RoundedRectangle(cornerRadius: 6))
} else {
let isDark = style == .dark
ZStack {
if isDark {
LinearGradient(colors: [.blue.opacity(0.8), .black], startPoint: .topLeading, endPoint: .bottomTrailing)
} else {
LinearGradient(colors: [.cyan.opacity(0.6), .white], startPoint: .topLeading, endPoint: .bottomTrailing)
}
WindowGraphic(isDark: isDark)
}
.frame(width: 72, height: 48)
.clipShape(RoundedRectangle(cornerRadius: 6))
}
if isSelected {
RoundedRectangle(cornerRadius: 8)
.stroke(Color.accentColor, lineWidth: 3)
.frame(width: 76, height: 52)
}
}
}
.buttonStyle(.plain)
Text(title)
.font(.system(size: 11, weight: isSelected ? .bold : .regular))
}
}
}
struct WindowGraphic: View {
let isDark: Bool
var body: some View {
VStack(spacing: 0) {
Spacer().frame(height: 12)
ZStack(alignment: .topLeading) {
RoundedRectangle(cornerRadius: 4)
.fill(isDark ? Color(white: 0.1) : .white)
.shadow(color: .black.opacity(0.2), radius: 2, y: 1)
// Content bar
RoundedRectangle(cornerRadius: 2)
.fill(Color.accentColor)
.frame(width: 40, height: 8)
.padding(.leading, 8)
.padding(.top, 8)
// Traffic lights
HStack(spacing: 2) {
Circle().fill(Color.red).frame(width: 4, height: 4)
Circle().fill(Color.yellow).frame(width: 4, height: 4)
Circle().fill(Color.green).frame(width: 4, height: 4)
}
.padding(.leading, 8)
.padding(.top, 24)
}
.frame(width: 56, height: 40)
}
}
}
struct LiquidGlassSelectionView: View {
@Binding var selection: Int
var body: some View {
HStack(spacing: 20) {
LiquidGlassThumbnail(title: "Transparent", isSelected: selection == 1, isTinted: false) {
selection = 1
}
LiquidGlassThumbnail(title: "Tinted", isSelected: selection == 0, isTinted: true) {
selection = 0
}
}
.padding(.vertical, 8)
}
}
struct LiquidGlassThumbnail: View {
let title: LocalizedStringKey
let isSelected: Bool
let isTinted: Bool
let action: () -> Void
@Environment(\.colorScheme) var colorScheme
var body: some View {
VStack(spacing: 8) {
Button(action: action) {
ZStack {
RoundedRectangle(cornerRadius: 10)
.fill(Color.clear)
.frame(width: 86, height: 60)
let isDark = colorScheme == .dark
ZStack {
if isDark {
LinearGradient(colors: [.blue.opacity(0.8), .black], startPoint: .topLeading, endPoint: .bottomTrailing)
} else {
LinearGradient(colors: [.cyan.opacity(0.6), .white], startPoint: .topLeading, endPoint: .bottomTrailing)
}
if isTinted {
RoundedRectangle(cornerRadius: 12)
.fill(isDark ? Color.accentColor.opacity(0.4) : Color(white: 1.0).opacity(0.7))
.background(.regularMaterial)
.shadow(color: .black.opacity(0.2), radius: 2, y: 1)
.frame(width: 50, height: 26)
.clipShape(RoundedRectangle(cornerRadius: 12))
} else {
RoundedRectangle(cornerRadius: 12)
.fill(Color.clear)
.background(.ultraThinMaterial)
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(Color(white: 1.0).opacity(isDark ? 0.2 : 0.6), lineWidth: 0.5)
)
.shadow(color: .black.opacity(0.2), radius: 2, y: 1)
.frame(width: 50, height: 26)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
.frame(width: 80, height: 54)
.clipShape(RoundedRectangle(cornerRadius: 8))
if isSelected {
RoundedRectangle(cornerRadius: 10)
.stroke(Color.accentColor, lineWidth: 3)
.frame(width: 86, height: 60)
}
}
}
.buttonStyle(.plain)
Text(title)
.font(.system(size: 11, weight: isSelected ? .bold : .regular))
}
}
}
struct WindowButtonsHider: NSViewRepresentable {
func makeNSView(context: Context) -> NSView {
let view = NSView()
DispatchQueue.main.async {
if let window = view.window {
window.standardWindowButton(.closeButton)?.isHidden = true
window.standardWindowButton(.miniaturizeButton)?.isHidden = true
window.standardWindowButton(.zoomButton)?.isHidden = true
}
}
return view
}
func updateNSView(_ nsView: NSView, context: Context) {}
}

View File

@ -10,7 +10,7 @@
"fill" : "automatic",
"glass" : true,
"hidden" : false,
"image-name" : "IMG_0959.PNG",
"image-name" : "IMG_0959.png",
"name" : "IMG_0959",
"opacity" : 1,
"position" : {

View File

@ -13,5 +13,5 @@
<choice id="dev.jesuschapman.Valentine" visible="false">
<pkg-ref id="dev.jesuschapman.Valentine"/>
</choice>
<pkg-ref id="dev.jesuschapman.Valentine" version="1.0 - 001" onConclusion="none">Valentine-component.pkg</pkg-ref>
<pkg-ref id="dev.jesuschapman.Valentine" version="1.1 - 002" onConclusion="none">Valentine-component.pkg</pkg-ref>
</installer-gui-script>