From a4103c82508f4e7665d37284761a043e38f642fa Mon Sep 17 00:00:00 2001 From: Dallas Groot Date: Fri, 10 Apr 2026 19:05:45 -0700 Subject: [PATCH] bug fixes and ready to upload to testflight --- Shared/Audio/AudioPlayer.swift | 3 + iOS/Views/Common/MainTabView.swift | 150 ++++++++++-------- iOS/Views/Library/SearchView.swift | 39 +++-- .../Visualizer/MitsuhaVisualizerView.swift | 33 +++- 4 files changed, 145 insertions(+), 80 deletions(-) diff --git a/Shared/Audio/AudioPlayer.swift b/Shared/Audio/AudioPlayer.swift index b632ce1..48fc473 100644 --- a/Shared/Audio/AudioPlayer.swift +++ b/Shared/Audio/AudioPlayer.swift @@ -840,6 +840,7 @@ class AudioPlayer: NSObject, ObservableObject { // Without this, the first 1-2 Canvas frames read stale zeroed _audioLevels // (zeroed by pause) and the wave starts flat then slowly ramps up. if isUsingOfflineVis { + alog("resume: offlineVis path — startOfflineVisSync", category: "VisDebug") // Offline vis: prime with the last-rendered frame at current position if !offlineVisBuffer.isEmpty { let dur = duration @@ -850,6 +851,7 @@ class AudioPlayer: NSObject, ObservableObject { } startOfflineVisSync() } else if isUsingRealFFT, !isUsingOfflineVis { + alog("resume: realFFT path — rebuilding levelTimer", category: "VisDebug") // Engine FFT path: rawFFTLevels still holds pre-pause data — push it immediately setLevels(rawFFTLevels) stopLevelTimer() @@ -858,6 +860,7 @@ class AudioPlayer: NSObject, ObservableObject { self.setLevels(self.rawFFTLevels) } } else { + alog("resume: simulation path — levelTimer nil=\(levelTimer == nil)", category: "VisDebug") // Simulation path: seed internalLevels from a fresh random target // so the wave has height immediately rather than starting from floor. lastTargetPhase = -1 diff --git a/iOS/Views/Common/MainTabView.swift b/iOS/Views/Common/MainTabView.swift index cd781e9..6774991 100644 --- a/iOS/Views/Common/MainTabView.swift +++ b/iOS/Views/Common/MainTabView.swift @@ -20,7 +20,14 @@ struct MainTabView: View { // Debug panel (docked) @State private var debugPanelHeight: CGFloat = 250 @State private var debugDragOffset: CGFloat = 0 - + @State private var showFullConsole = false + + // Group toggles — same @AppStorage keys as DebugConsoleView so state is shared + @AppStorage("dbg_show_iphone") private var dbgShowIPhone = true + @AppStorage("dbg_show_watch") private var dbgShowWatch = true + @AppStorage("dbg_show_companion") private var dbgShowCompanion = true + @AppStorage("dbg_show_audio") private var dbgShowAudio = true + // Debug PiP (floating) @State private var debugPipMode = true @State private var pipPosition: CGPoint = CGPoint(x: 16, y: 100) @@ -207,7 +214,7 @@ struct MainTabView: View { } // MARK: - Debug Panel - + private var debugPanel: some View { VStack(spacing: 0) { // Drag handle + header @@ -216,86 +223,62 @@ struct MainTabView: View { .fill(Color.white.opacity(0.4)) .frame(width: 36, height: 4) .padding(.top, 8) - + HStack { - Image(systemName: "ladybug.fill") - .font(.system(size: 12)) - .foregroundColor(.red) - Text("Debug Console") - .font(.system(size: 13, weight: .semibold)) - .foregroundColor(.white) - - Text("\(DebugLogger.shared.entries.count)") + Image(systemName: "ladybug.fill").font(.system(size: 12)).foregroundColor(.red) + Text("Debug Console").font(.system(size: 13, weight: .semibold)).foregroundColor(.white) + Text("\(filteredDebugEntries.count)/\(DebugLogger.shared.entries.count)") .font(.system(size: 10, weight: .medium, design: .monospaced)) .foregroundColor(.gray) - .padding(.horizontal, 6) - .padding(.vertical, 2) - .background(Color.white.opacity(0.1)) - .cornerRadius(8) - + .padding(.horizontal, 6).padding(.vertical, 2) + .background(Color.white.opacity(0.1)).cornerRadius(8) Spacer() - Button(action: { DebugLogger.shared.clear() }) { - Text("Clear") - .font(.system(size: 12, weight: .medium)) - .foregroundColor(.red.opacity(0.8)) + Text("Clear").font(.system(size: 12, weight: .medium)).foregroundColor(.red.opacity(0.8)) } - - Button(action: { - withAnimation(.easeInOut(duration: 0.3)) { - debugPipMode = true - } - }) { - Image(systemName: "pip") - .font(.system(size: 14)) - .foregroundColor(.white.opacity(0.6)) + Button(action: { withAnimation(.easeInOut(duration: 0.3)) { debugPipMode = true } }) { + Image(systemName: "pip").font(.system(size: 14)).foregroundColor(.white.opacity(0.6)) } - - Button(action: { - debugLogger.isEnabled = false - }) { - Image(systemName: "xmark.circle.fill") - .font(.system(size: 16)) - .foregroundColor(.gray) + // Open full console with all controls + Button(action: { showFullConsole = true }) { + Image(systemName: "arrow.up.left.and.arrow.down.right") + .font(.system(size: 13)).foregroundColor(.white.opacity(0.6)) + } + Button(action: { debugLogger.isEnabled = false }) { + Image(systemName: "xmark.circle.fill").font(.system(size: 16)).foregroundColor(.gray) } } - .padding(.horizontal, 14) + .padding(.horizontal, 14).padding(.bottom, 4) + + // ── Group toggles ────────────────────────────────────────── + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 6) { + dbgGroupToggle("iPhone", icon: "iphone", isOn: $dbgShowIPhone) + dbgGroupToggle("Watch", icon: "applewatch", isOn: $dbgShowWatch) + dbgGroupToggle("Companion", icon: "server.rack", isOn: $dbgShowCompanion) + dbgGroupToggle("Audio", icon: "waveform", isOn: $dbgShowAudio) + } + .padding(.horizontal, 14) + } .padding(.bottom, 6) } .background(Color(white: 0.1)) - .gesture( - DragGesture() - .onChanged { value in - debugDragOffset = value.translation.height - } - .onEnded { value in - let newHeight = debugPanelHeight - value.translation.height - withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { - debugDragOffset = 0 - debugPanelHeight = min(max(newHeight, 120), 500) - } - } - ) - + Divider().background(Color.white.opacity(0.15)) - - // Log entries + + // Log entries — filtered by group toggles ScrollViewReader { proxy in ScrollView { LazyVStack(alignment: .leading, spacing: 1) { - ForEach(DebugLogger.shared.entries) { entry in - debugLogRow(entry) - .id(entry.id) + ForEach(filteredDebugEntries) { entry in + debugLogRow(entry).id(entry.id) } } - .padding(.horizontal, 10) - .padding(.vertical, 4) + .padding(.horizontal, 10).padding(.vertical, 4) } .onChange(of: DebugLogger.shared.entries.count) { _, _ in - if let last = DebugLogger.shared.entries.last { - withAnimation(.easeOut(duration: 0.15)) { - proxy.scrollTo(last.id, anchor: .bottom) - } + if let last = filteredDebugEntries.last { + withAnimation(.easeOut(duration: 0.15)) { proxy.scrollTo(last.id, anchor: .bottom) } } } } @@ -305,7 +288,48 @@ struct MainTabView: View { .clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous)) .shadow(color: .black.opacity(0.4), radius: 8, y: -2) .padding(.horizontal, 4) - .padding(.bottom, 49) // above tab bar + .padding(.bottom, 49) + .sheet(isPresented: $showFullConsole) { DebugConsoleView() } + .gesture( + DragGesture() + .onChanged { value in debugDragOffset = value.translation.height } + .onEnded { value in + let newHeight = debugPanelHeight - value.translation.height + withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { + debugDragOffset = 0 + debugPanelHeight = min(max(newHeight, 120), 500) + } + } + ) + } + + // Filtered entries for the docked panel using the same group toggle AppStorage keys + private var filteredDebugEntries: [DebugLogger.LogEntry] { + let watchCats: Set = ["Watch", "WC", "WatchSync"] + let companionCats: Set = ["Companion", "SmartDJ", "Crossfade", "Sync"] + let audioCats: Set = ["Audio", "FFT", "Radio", "Prefetch"] + return DebugLogger.shared.entries.filter { entry in + if entry.isMarker { return true } + let isWatch = watchCats.contains(entry.category) + let isCompanion = companionCats.contains(entry.category) + let isAudio = audioCats.contains(entry.category) + let isIPhone = !isWatch && !isCompanion && !isAudio + return (isIPhone && dbgShowIPhone) || (isWatch && dbgShowWatch) || + (isCompanion && dbgShowCompanion) || (isAudio && dbgShowAudio) + } + } + + private func dbgGroupToggle(_ title: String, icon: String, isOn: Binding) -> some View { + Button(action: { isOn.wrappedValue.toggle() }) { + HStack(spacing: 4) { + Image(systemName: icon).font(.system(size: 10)) + Text(title).font(.system(size: 10, weight: .medium)) + } + .foregroundColor(isOn.wrappedValue ? .white : .gray.opacity(0.4)) + .padding(.horizontal, 8).padding(.vertical, 4) + .background(isOn.wrappedValue ? accentPink.opacity(0.4) : Color.white.opacity(0.05)) + .cornerRadius(10) + } } private func debugLogRow(_ entry: DebugLogger.LogEntry) -> some View { diff --git a/iOS/Views/Library/SearchView.swift b/iOS/Views/Library/SearchView.swift index 851af79..4a407e5 100644 --- a/iOS/Views/Library/SearchView.swift +++ b/iOS/Views/Library/SearchView.swift @@ -372,35 +372,42 @@ struct SearchView: View { guard !songsLoaded else { return } isLoadingSongs = true - // Try loading from cache first for instant display + // Cache hit → instant display if let cached = libraryCache.load([Song].self, key: "all_songs_sorted") { await MainActor.run { - allSongs = cached + allSongs = cached isLoadingSongs = false - songsLoaded = true + songsLoaded = true } return } - // Fetch all albums, then their songs, sorted alphabetically by title + // Use search3 with empty query to fetch all songs directly. + // This is a single paginated call per 500 songs vs. 1 call per album + // (144 albums = 144 sequential requests, many of which fail silently). do { - var offset = 0 var collected: [Song] = [] + var offset = 0 + let pageSize = 500 while true { - let albums = try await serverManager.client.getAlbumList2( - type: "alphabeticalByName", size: 500, offset: offset) - for album in albums { - if let detail = try? await serverManager.client.getAlbum(id: album.id) { - collected.append(contentsOf: detail.song ?? []) - } - } - if albums.count < 500 { break } - offset += 500 + let result = try await serverManager.client.search3( + query: "", + artistCount: 0, artistOffset: 0, + albumCount: 0, albumOffset: 0, + songCount: pageSize, + songOffset: offset + ) + let page = result?.song ?? [] + collected.append(contentsOf: page) + if page.count < pageSize { break } + offset += pageSize + } + let sorted = collected.sorted { + $0.title.localizedCaseInsensitiveCompare($1.title) == .orderedAscending } - let sorted = collected.sorted { $0.title.localizedCaseInsensitiveCompare($1.title) == .orderedAscending } libraryCache.save(sorted, key: "all_songs_sorted") await MainActor.run { - allSongs = sorted + allSongs = sorted isLoadingSongs = false songsLoaded = true } diff --git a/iOS/Views/Visualizer/MitsuhaVisualizerView.swift b/iOS/Views/Visualizer/MitsuhaVisualizerView.swift index c9c3afa..57973c7 100644 --- a/iOS/Views/Visualizer/MitsuhaVisualizerView.swift +++ b/iOS/Views/Visualizer/MitsuhaVisualizerView.swift @@ -256,6 +256,7 @@ fileprivate final class VisualizerLevelBox: ObservableObject { var historyWriteIdx: Int = 0 var wobblePhaseOffset: Double = 0 var lastTickTime: CFTimeInterval = 0 + var resumeTickCount: Int = 0 // debug: counts first N ticks after resume /// Resize all per-frame buffers when point count changes (rare — settings slider). func resizeIfNeeded(count: Int, idleAmplitude: Float) { @@ -314,6 +315,16 @@ struct MitsuhaVisualizerView: View { idleAmplitude: Float(settings.idleAmplitude)) let t = CACurrentMediaTime() let rawLevels = previewLevels ?? AudioPlayer.shared.currentLevels() + // Log first 3 ticks after resume so we can see the TimelineView IS firing + if box.resumeTickCount < 3 { + box.resumeTickCount += 1 + DebugLogger.shared.log( + "TL tick #\(box.resumeTickCount): " + + "rawCount=\(rawLevels.count) " + + "rawMax=\(String(format: "%.3f", rawLevels.max() ?? 0)) " + + "lastTick=\(String(format: "%.1f", box.lastTickTime))", + category: "VisDebug", level: .debug) + } updateDisplayLevels(newRawLevels: rawLevels, t: t) updateWobblePhase(t: t) guard box.displayLevels.count >= 2 else { return } @@ -353,6 +364,19 @@ struct MitsuhaVisualizerView: View { box.levelHistoryBuf.removeAll(keepingCapacity: true) box.historyWriteIdx = 0 box.peakFollower = 0.01 + box.lastTickTime = 0 // ← reset so first resume frame uses 1/60 fallback dt + DebugLogger.shared.log( + "PAUSE → levelHistoryBuf cleared, lastTickTime reset " + + "[targets:\(box.targetLevels.count) display:\(box.displayLevels.count)]", + category: "VisDebug", level: .info) + } else { + box.resumeTickCount = 0 // reset so we log the first 3 ticks + DebugLogger.shared.log( + "RESUME → isRenderingActive=\(isRenderingActive) " + + "isAppActive=\(isAppActive) isVisible=\(isVisible) " + + "[targets:\(box.targetLevels.count) display:\(box.displayLevels.count) " + + "history:\(box.levelHistoryBuf.count)]", + category: "VisDebug", level: .info) } } .onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in @@ -394,7 +418,14 @@ struct MitsuhaVisualizerView: View { private func updateDisplayLevels(newRawLevels: [Float], t: Double) -> Bool { let count = config.numberOfPoints guard count > 0, !newRawLevels.isEmpty, - box.targetLevels.count == count else { return false } + box.targetLevels.count == count else { + DebugLogger.shared.log( + "updateDisplayLevels GUARD FAILED: " + + "count=\(count) rawEmpty=\(newRawLevels.isEmpty) " + + "targetCount=\(box.targetLevels.count)", + category: "VisDebug", level: .warning) + return false + } let dt = Float(box.lastTickTime > 0 ? min(t - box.lastTickTime, 0.1) : 1.0/60.0) let sens = Float(config.sensitivity)