From 1eba9b60cb379a08d9d08720fe1c0f2b1e2384cd Mon Sep 17 00:00:00 2001 From: Magesh K <47920326+mahee96@users.noreply.github.com> Date: Tue, 14 Jan 2025 01:30:31 +0530 Subject: [PATCH] [ConsoleLogView]: Feature: Added capability to search the logs --- .../Settings/Error Log/ConsoleLogView.swift | 167 ++++++++++++++---- 1 file changed, 135 insertions(+), 32 deletions(-) diff --git a/AltStore/Settings/Error Log/ConsoleLogView.swift b/AltStore/Settings/Error Log/ConsoleLogView.swift index 36568db3..772560cf 100644 --- a/AltStore/Settings/Error Log/ConsoleLogView.swift +++ b/AltStore/Settings/Error Log/ConsoleLogView.swift @@ -10,6 +10,10 @@ import SwiftUI class ConsoleLogViewModel: ObservableObject { @Published var logLines: [String] = [] + @Published var searchTerm: String = "" + @Published var currentSearchIndex: Int = 0 + @Published var searchResults: [Int] = [] // Stores indices of matching lines + private var fileWatcher: DispatchSourceFileSystemObject? private let backgroundQueue = DispatchQueue(label: "com.myapp.backgroundQueue", qos: .background) @@ -47,15 +51,40 @@ class ConsoleLogViewModel: ObservableObject { deinit { fileWatcher?.cancel() } + + + func performSearch() { + searchResults = logLines.enumerated() + .filter { $0.element.localizedCaseInsensitiveContains(searchTerm) } + .map { $0.offset } + } + + func nextSearchResult() { + guard !searchResults.isEmpty else { return } + currentSearchIndex = (currentSearchIndex + 1) % searchResults.count + } + + func previousSearchResult() { + guard !searchResults.isEmpty else { return } + currentSearchIndex = (currentSearchIndex - 1 + searchResults.count) % searchResults.count + } } public struct ConsoleLogView: View { @ObservedObject var viewModel: ConsoleLogViewModel - @State private var isAtBottom: Bool = true -// private let linesToShow: Int = 100 // Number of lines to show at once @State private var scrollToBottom: Bool = false // State variable to trigger scroll + @State private var searchBarState: Bool = false + @FocusState private var isSearchFieldFocused: Bool + + @State private var searchText: String = "" + @State private var scrollToIndex: Int? + + private let resultHighlightColor = Color.orange + private let resultHighlightOpacity = 0.5 + private let otherResultsColor = Color.yellow + private let otherResultsOpacity = 0.3 init(logURL: URL) { self.viewModel = ConsoleLogViewModel(logURL: logURL) @@ -63,12 +92,24 @@ public struct ConsoleLogView: View { public var body: some View { VStack { + // Custom Header Bar (similar to QuickLook's preview screen) HStack { Text("Console Log") .font(.system(size: 22, weight: .semibold)) .foregroundColor(.white) Spacer() + + if(!searchBarState){ + SwiftUI.Button(action: { + searchBarState.toggle() + }) { + Image(systemName: "magnifyingglass") + .foregroundColor(.white) + .imageScale(.large) + } + .padding(.trailing) + } SwiftUI.Button(action: { scrollToBottom.toggle() }) { @@ -87,37 +128,99 @@ public struct ConsoleLogView: View { .foregroundColor(Color.gray.opacity(0.2)), alignment: .bottom ) - // Main Log Display (scrollable area) - ScrollView(.vertical) { - ScrollViewReader { scrollViewProxy in - LazyVStack(alignment: .leading, spacing: 4) { - ForEach(viewModel.logLines.indices, id: \.self) { index in - Text(viewModel.logLines[index]) - .font(.system(size: 12, design: .monospaced)) - .foregroundColor(.white) - } - } - .onChange(of: scrollToBottom) { _ in - scrollToBottomIfNeeded(scrollViewProxy: scrollViewProxy) - } - } + if(searchBarState){ + // Search bar + HStack { + Image(systemName: "magnifyingglass") + .foregroundColor(.gray) + .padding(.trailing, 4) + + TextField("Search", text: $searchText) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .onChange(of: searchText) { newValue in + viewModel.searchTerm = newValue + viewModel.performSearch() + } + .keyboardShortcut("f", modifiers: .command) // Focus search field + + if !searchText.isEmpty { + // Search navigation buttons + SwiftUI.Button(action: { + viewModel.previousSearchResult() + scrollToIndex = viewModel.searchResults[viewModel.currentSearchIndex] + }) { + Image(systemName: "chevron.up") + } + .keyboardShortcut(.return, modifiers: [.command, .shift]) + .disabled(viewModel.searchResults.isEmpty) + + SwiftUI.Button(action: { + viewModel.nextSearchResult() + scrollToIndex = viewModel.searchResults[viewModel.currentSearchIndex] + }) { + Image(systemName: "chevron.down") + } + .keyboardShortcut(.return, modifiers: .command) + .disabled(viewModel.searchResults.isEmpty) + + // Results counter + Text("\(viewModel.currentSearchIndex + 1)/\(viewModel.searchResults.count)") + .foregroundColor(.gray) + .font(.caption) + } + + SwiftUI.Button(action: { + searchBarState.toggle() + }) { + Image(systemName: "xmark") + } + } + .padding(.horizontal, 15) } - } - .background(Color.black) // Set background color to mimic QL's dark theme - .edgesIgnoringSafeArea(.all) - } - - // Scroll to the last index (bottom) only if logLines is not empty - private func scrollToBottomIfNeeded(scrollViewProxy: ScrollViewProxy) { - // Ensure we have log data before attempting to scroll - guard !viewModel.logLines.isEmpty else { - return + + + + // Main Log Display (scrollable area) + ScrollView(.vertical) { + ScrollViewReader { proxy in + LazyVStack(alignment: .leading, spacing: 4) { + ForEach(viewModel.logLines.indices, id: \.self) { index in + Text(viewModel.logLines[index]) + .font(.system(size: 12, design: .monospaced)) + .foregroundColor(.white) + .background( + viewModel.searchResults.contains(index) ? + otherResultsColor.opacity(otherResultsOpacity) : Color.clear + ) + .background( + viewModel.searchResults[safe: viewModel.currentSearchIndex] == index ? + resultHighlightColor.opacity(resultHighlightOpacity) : Color.clear + ) + } + } + .onChange(of: scrollToIndex) { newIndex in + if let index = newIndex { + withAnimation { + proxy.scrollTo(index, anchor: .center) + } + } + } + .onChange(of: scrollToBottom) { _ in + viewModel.logLines.indices.last.map { last in + proxy.scrollTo(last, anchor: .bottom) + } + } + } + } } - - let last = viewModel.logLines.count - 1 - let lastIdx = viewModel.logLines.indices.last - assert(last == lastIdx) -// scrollViewProxy.scrollTo(lastIdx, anchor: .bottom) - scrollViewProxy.scrollTo(last, anchor: .bottom) + .background(Color.black) // Set background color to mimic QL's dark theme + .edgesIgnoringSafeArea(.all) + } +} + +// Helper extension for safe array access +extension Array { + subscript(safe index: Index) -> Element? { + indices.contains(index) ? self[index] : nil } }