mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-09 06:43:25 +01:00
[ConsoleLogView]: Feature: Added capability to search the logs
This commit is contained in:
@@ -10,6 +10,10 @@ import SwiftUI
|
|||||||
class ConsoleLogViewModel: ObservableObject {
|
class ConsoleLogViewModel: ObservableObject {
|
||||||
@Published var logLines: [String] = []
|
@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 var fileWatcher: DispatchSourceFileSystemObject?
|
||||||
|
|
||||||
private let backgroundQueue = DispatchQueue(label: "com.myapp.backgroundQueue", qos: .background)
|
private let backgroundQueue = DispatchQueue(label: "com.myapp.backgroundQueue", qos: .background)
|
||||||
@@ -47,15 +51,40 @@ class ConsoleLogViewModel: ObservableObject {
|
|||||||
deinit {
|
deinit {
|
||||||
fileWatcher?.cancel()
|
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 {
|
public struct ConsoleLogView: View {
|
||||||
|
|
||||||
@ObservedObject var viewModel: ConsoleLogViewModel
|
@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 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) {
|
init(logURL: URL) {
|
||||||
self.viewModel = ConsoleLogViewModel(logURL: logURL)
|
self.viewModel = ConsoleLogViewModel(logURL: logURL)
|
||||||
@@ -63,12 +92,24 @@ public struct ConsoleLogView: View {
|
|||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
|
|
||||||
// Custom Header Bar (similar to QuickLook's preview screen)
|
// Custom Header Bar (similar to QuickLook's preview screen)
|
||||||
HStack {
|
HStack {
|
||||||
Text("Console Log")
|
Text("Console Log")
|
||||||
.font(.system(size: 22, weight: .semibold))
|
.font(.system(size: 22, weight: .semibold))
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
|
if(!searchBarState){
|
||||||
|
SwiftUI.Button(action: {
|
||||||
|
searchBarState.toggle()
|
||||||
|
}) {
|
||||||
|
Image(systemName: "magnifyingglass")
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.imageScale(.large)
|
||||||
|
}
|
||||||
|
.padding(.trailing)
|
||||||
|
}
|
||||||
SwiftUI.Button(action: {
|
SwiftUI.Button(action: {
|
||||||
scrollToBottom.toggle()
|
scrollToBottom.toggle()
|
||||||
}) {
|
}) {
|
||||||
@@ -87,37 +128,99 @@ public struct ConsoleLogView: View {
|
|||||||
.foregroundColor(Color.gray.opacity(0.2)), alignment: .bottom
|
.foregroundColor(Color.gray.opacity(0.2)), alignment: .bottom
|
||||||
)
|
)
|
||||||
|
|
||||||
// Main Log Display (scrollable area)
|
if(searchBarState){
|
||||||
ScrollView(.vertical) {
|
// Search bar
|
||||||
ScrollViewReader { scrollViewProxy in
|
HStack {
|
||||||
LazyVStack(alignment: .leading, spacing: 4) {
|
Image(systemName: "magnifyingglass")
|
||||||
ForEach(viewModel.logLines.indices, id: \.self) { index in
|
.foregroundColor(.gray)
|
||||||
Text(viewModel.logLines[index])
|
.padding(.trailing, 4)
|
||||||
.font(.system(size: 12, design: .monospaced))
|
|
||||||
.foregroundColor(.white)
|
TextField("Search", text: $searchText)
|
||||||
}
|
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||||
}
|
.onChange(of: searchText) { newValue in
|
||||||
.onChange(of: scrollToBottom) { _ in
|
viewModel.searchTerm = newValue
|
||||||
scrollToBottomIfNeeded(scrollViewProxy: scrollViewProxy)
|
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)
|
|
||||||
}
|
// Main Log Display (scrollable area)
|
||||||
|
ScrollView(.vertical) {
|
||||||
// Scroll to the last index (bottom) only if logLines is not empty
|
ScrollViewReader { proxy in
|
||||||
private func scrollToBottomIfNeeded(scrollViewProxy: ScrollViewProxy) {
|
LazyVStack(alignment: .leading, spacing: 4) {
|
||||||
// Ensure we have log data before attempting to scroll
|
ForEach(viewModel.logLines.indices, id: \.self) { index in
|
||||||
guard !viewModel.logLines.isEmpty else {
|
Text(viewModel.logLines[index])
|
||||||
return
|
.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.background(Color.black) // Set background color to mimic QL's dark theme
|
||||||
let last = viewModel.logLines.count - 1
|
.edgesIgnoringSafeArea(.all)
|
||||||
let lastIdx = viewModel.logLines.indices.last
|
}
|
||||||
assert(last == lastIdx)
|
}
|
||||||
// scrollViewProxy.scrollTo(lastIdx, anchor: .bottom)
|
|
||||||
scrollViewProxy.scrollTo(last, anchor: .bottom)
|
// Helper extension for safe array access
|
||||||
|
extension Array {
|
||||||
|
subscript(safe index: Index) -> Element? {
|
||||||
|
indices.contains(index) ? self[index] : nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user