mirror of
https://github.com/StikDebug/StikDebug.git
synced 2026-10-15 20:10:30 +00:00
Syslogs
This commit is contained in:
parent
586841ba1b
commit
42142c80ce
5 changed files with 359 additions and 89 deletions
|
|
@ -74,6 +74,8 @@ enum DistributorType: String {
|
||||||
@MainActor
|
@MainActor
|
||||||
final class ThemeExpansionManager: ObservableObject {
|
final class ThemeExpansionManager: ObservableObject {
|
||||||
static let productIdentifier = "SD_Theme_Expansion"
|
static let productIdentifier = "SD_Theme_Expansion"
|
||||||
|
static let comingSoonMessage = "Theme Expansion is coming soon on this store."
|
||||||
|
static let unavailableMessage = "Theme Expansion isn’t available."
|
||||||
|
|
||||||
@Published private(set) var hasThemeExpansion = false
|
@Published private(set) var hasThemeExpansion = false
|
||||||
@Published private(set) var themeExpansionProduct: Product?
|
@Published private(set) var themeExpansionProduct: Product?
|
||||||
|
|
@ -84,6 +86,13 @@ final class ThemeExpansionManager: ObservableObject {
|
||||||
// New: distribution awareness
|
// New: distribution awareness
|
||||||
@Published private(set) var distributor: DistributorType
|
@Published private(set) var distributor: DistributorType
|
||||||
var isAppStoreBuild: Bool { distributor == .appStore }
|
var isAppStoreBuild: Bool { distributor == .appStore }
|
||||||
|
var shouldShowThemeExpansionUpsell: Bool {
|
||||||
|
guard isAppStoreBuild else { return false }
|
||||||
|
if let lastError, lastError == Self.unavailableMessage {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
private var updatesTask: Task<Void, Never>?
|
private var updatesTask: Task<Void, Never>?
|
||||||
private let isPreviewInstance: Bool
|
private let isPreviewInstance: Bool
|
||||||
|
|
@ -163,7 +172,7 @@ final class ThemeExpansionManager: ObservableObject {
|
||||||
func restorePurchases() async {
|
func restorePurchases() async {
|
||||||
guard !isPreviewInstance else { return }
|
guard !isPreviewInstance else { return }
|
||||||
guard isAppStoreBuild else {
|
guard isAppStoreBuild else {
|
||||||
lastError = "Theme Expansion is coming soon on this store."
|
lastError = Self.comingSoonMessage
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
isProcessing = true
|
isProcessing = true
|
||||||
|
|
@ -180,7 +189,7 @@ final class ThemeExpansionManager: ObservableObject {
|
||||||
func purchaseThemeExpansion() async {
|
func purchaseThemeExpansion() async {
|
||||||
guard !isPreviewInstance else { return }
|
guard !isPreviewInstance else { return }
|
||||||
guard isAppStoreBuild else {
|
guard isAppStoreBuild else {
|
||||||
lastError = "Theme Expansion is coming soon on this store."
|
lastError = Self.comingSoonMessage
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -195,13 +204,9 @@ final class ThemeExpansionManager: ObservableObject {
|
||||||
product = first
|
product = first
|
||||||
} else {
|
} else {
|
||||||
#if targetEnvironment(simulator)
|
#if targetEnvironment(simulator)
|
||||||
lastError = """
|
lastError = Self.unavailableMessage
|
||||||
Theme Expansion isn’t available.
|
|
||||||
"""
|
|
||||||
#else
|
#else
|
||||||
lastError = """
|
lastError = Self.unavailableMessage
|
||||||
Theme Expansion isn’t available.
|
|
||||||
"""
|
|
||||||
#endif
|
#endif
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,11 @@ import SwiftUI
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
struct ConsoleLogsView: View {
|
struct ConsoleLogsView: View {
|
||||||
@Environment(\.dismiss) private var dismiss
|
|
||||||
@Environment(\.colorScheme) private var colorScheme
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
@Environment(\.accentColor) private var environmentAccentColor
|
@Environment(\.accentColor) private var environmentAccentColor
|
||||||
@StateObject private var logManager = LogManager.shared
|
@StateObject private var logManager = LogManager.shared
|
||||||
|
@StateObject private var systemLogStream = SystemLogStream()
|
||||||
|
@State private var selectedConsoleTab: ConsoleTab = .idevice
|
||||||
@State private var jitScrollView: ScrollViewProxy? = nil
|
@State private var jitScrollView: ScrollViewProxy? = nil
|
||||||
@AppStorage("customAccentColor") private var customAccentColorHex: String = ""
|
@AppStorage("customAccentColor") private var customAccentColorHex: String = ""
|
||||||
|
|
||||||
|
|
@ -27,10 +28,14 @@ struct ConsoleLogsView: View {
|
||||||
@State private var lastProcessedLineCount = 0
|
@State private var lastProcessedLineCount = 0
|
||||||
@State private var isLoadingLogs = false
|
@State private var isLoadingLogs = false
|
||||||
@State private var jitIsAtBottom = true
|
@State private var jitIsAtBottom = true
|
||||||
|
@State private var syslogIsAtBottom = true
|
||||||
|
@State private var showingSyslogSpeedSelector = false
|
||||||
|
private let appLogRefreshInterval: TimeInterval = 3.0
|
||||||
@AppStorage("appTheme") private var appThemeRaw: String = AppTheme.system.rawValue
|
@AppStorage("appTheme") private var appThemeRaw: String = AppTheme.system.rawValue
|
||||||
@Environment(\.themeExpansionManager) private var themeExpansion
|
@Environment(\.themeExpansionManager) private var themeExpansion
|
||||||
private var backgroundStyle: BackgroundStyle { themeExpansion?.backgroundStyle(for: appThemeRaw) ?? AppTheme.system.backgroundStyle }
|
private var backgroundStyle: BackgroundStyle { themeExpansion?.backgroundStyle(for: appThemeRaw) ?? AppTheme.system.backgroundStyle }
|
||||||
private var preferredScheme: ColorScheme? { themeExpansion?.preferredColorScheme(for: appThemeRaw) }
|
private var preferredScheme: ColorScheme? { themeExpansion?.preferredColorScheme(for: appThemeRaw) }
|
||||||
|
private let syslogIntervalOptions: [Double] = [0.0, 0.2, 0.5, 1.0, 1.5, 2.0]
|
||||||
|
|
||||||
private var accentColor: Color {
|
private var accentColor: Color {
|
||||||
themeExpansion?.resolvedAccentColor(from: customAccentColorHex) ?? .blue
|
themeExpansion?.resolvedAccentColor(from: customAccentColorHex) ?? .blue
|
||||||
|
|
@ -50,46 +55,26 @@ struct ConsoleLogsView: View {
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
|
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
jitLogsPane
|
Group {
|
||||||
|
if selectedConsoleTab == .idevice {
|
||||||
|
jitLogsPane
|
||||||
|
} else {
|
||||||
|
syslogLogsPane
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(minLength: 0)
|
Spacer(minLength: 0)
|
||||||
jitFooter
|
|
||||||
|
Group {
|
||||||
|
if selectedConsoleTab == .idevice {
|
||||||
|
jitFooter
|
||||||
|
} else {
|
||||||
|
syslogFooter
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.padding(.top, 12)
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar {
|
|
||||||
ToolbarItem(placement: .principal) {
|
|
||||||
Text("Console Logs")
|
|
||||||
.font(.headline)
|
|
||||||
.foregroundColor(colorScheme == .dark ? .white : .black)
|
|
||||||
}
|
|
||||||
|
|
||||||
ToolbarItem(placement: .navigationBarLeading) {
|
|
||||||
Button(action: { dismiss() }) {
|
|
||||||
HStack(spacing: 2) {
|
|
||||||
Text("Exit")
|
|
||||||
.fontWeight(.regular)
|
|
||||||
}
|
|
||||||
.foregroundColor(accentColor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
|
||||||
HStack {
|
|
||||||
Button(action: {
|
|
||||||
Task { await loadIdeviceLogsAsync() }
|
|
||||||
}) {
|
|
||||||
Image(systemName: "arrow.clockwise")
|
|
||||||
.foregroundColor(accentColor)
|
|
||||||
}
|
|
||||||
|
|
||||||
Button(action: {
|
|
||||||
logManager.clearLogs()
|
|
||||||
}) {
|
|
||||||
Text("Clear")
|
|
||||||
.foregroundColor(accentColor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.overlay(
|
.overlay(
|
||||||
Group {
|
Group {
|
||||||
|
|
@ -109,10 +94,71 @@ struct ConsoleLogsView: View {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if showingSyslogSpeedSelector {
|
||||||
|
Color.black.opacity(0.45)
|
||||||
|
.ignoresSafeArea()
|
||||||
|
.onTapGesture { showingSyslogSpeedSelector = false }
|
||||||
|
.overlay(
|
||||||
|
appGlassCard {
|
||||||
|
VStack(spacing: 18) {
|
||||||
|
HStack {
|
||||||
|
Text("Syslog Speed")
|
||||||
|
.font(.title3.weight(.semibold))
|
||||||
|
Spacer()
|
||||||
|
Button {
|
||||||
|
showingSyslogSpeedSelector = false
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "xmark.circle.fill")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
Text("Choose how quickly new relay entries appear.")
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
|
||||||
|
VStack(spacing: 10) {
|
||||||
|
ForEach(syslogIntervalOptions, id: \.self) { option in
|
||||||
|
Button {
|
||||||
|
systemLogStream.updateInterval = option
|
||||||
|
showingSyslogSpeedSelector = false
|
||||||
|
} label: {
|
||||||
|
HStack {
|
||||||
|
Text(intervalLabel(for: option))
|
||||||
|
.font(.headline)
|
||||||
|
Spacer()
|
||||||
|
if abs(systemLogStream.updateInterval - option) < 0.01 {
|
||||||
|
Image(systemName: "checkmark.circle.fill")
|
||||||
|
.foregroundColor(accentColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 18)
|
||||||
|
.frame(maxWidth: .infinity, minHeight: 44)
|
||||||
|
}
|
||||||
|
.buttonStyle(
|
||||||
|
GlassOvalButtonStyle(
|
||||||
|
height: 44,
|
||||||
|
strokeOpacity: abs(systemLogStream.updateInterval - option) < 0.01 ? 0.35 : 0.15
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 30)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.preferredColorScheme(preferredScheme)
|
.preferredColorScheme(preferredScheme)
|
||||||
|
.onAppear {
|
||||||
|
startHeartbeatInBackground()
|
||||||
|
}
|
||||||
|
.onDisappear {
|
||||||
|
systemLogStream.stop()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var jitLogsPane: some View {
|
private var jitLogsPane: some View {
|
||||||
|
|
@ -189,18 +235,81 @@ struct ConsoleLogsView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var syslogLogsPane: some View {
|
||||||
|
ScrollViewReader { proxy in
|
||||||
|
ScrollView {
|
||||||
|
LazyVStack(alignment: .leading, spacing: 4) {
|
||||||
|
ForEach(systemLogStream.entries) { entry in
|
||||||
|
Text(AttributedString(createSyslogAttributedString(entry)))
|
||||||
|
.font(.system(size: 11, design: .monospaced))
|
||||||
|
.textSelection(.enabled)
|
||||||
|
.lineLimit(nil)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.padding(.vertical, 1)
|
||||||
|
.padding(.horizontal, 4)
|
||||||
|
.id(entry.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.background(
|
||||||
|
GeometryReader { geometry in
|
||||||
|
Color.clear.preference(
|
||||||
|
key: ScrollOffsetPreferenceKey.self,
|
||||||
|
value: geometry.frame(in: .named("syslogScroll")).minY
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.coordinateSpace(name: "syslogScroll")
|
||||||
|
.onPreferenceChange(ScrollOffsetPreferenceKey.self) { offset in
|
||||||
|
syslogIsAtBottom = offset > -20
|
||||||
|
}
|
||||||
|
.onChange(of: systemLogStream.entries.count) { _ in
|
||||||
|
guard syslogIsAtBottom, let lastLog = systemLogStream.entries.last else { return }
|
||||||
|
withAnimation {
|
||||||
|
proxy.scrollTo(lastLog.id, anchor: .bottom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
if selectedConsoleTab == .syslog && !systemLogStream.isStreaming {
|
||||||
|
systemLogStream.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onDisappear {
|
||||||
|
systemLogStream.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var jitFooter: some View {
|
private var jitFooter: some View {
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
HStack(spacing: 6) {
|
HStack(spacing: 6) {
|
||||||
Image(systemName: "exclamationmark.triangle.fill")
|
Image(systemName: "exclamationmark.triangle.fill")
|
||||||
.imageScale(.small)
|
.imageScale(.small)
|
||||||
.foregroundColor(.red)
|
.foregroundColor(.red)
|
||||||
Text("\(logManager.errorCount) Errors")
|
Text("\(logManager.errorCount)")
|
||||||
.font(.subheadline)
|
.font(.subheadline.weight(.semibold))
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
Spacer(minLength: 8)
|
Spacer(minLength: 8)
|
||||||
|
|
||||||
|
Button {
|
||||||
|
Task { await loadIdeviceLogsAsync() }
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "arrow.clockwise")
|
||||||
|
.foregroundColor(accentColor)
|
||||||
|
}
|
||||||
|
.buttonStyle(GlassOvalButtonStyle(height: 36, strokeOpacity: 0.18))
|
||||||
|
.accessibilityLabel("Refresh app logs")
|
||||||
|
|
||||||
|
Button {
|
||||||
|
logManager.clearLogs()
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "trash")
|
||||||
|
.foregroundColor(accentColor)
|
||||||
|
}
|
||||||
|
.buttonStyle(GlassOvalButtonStyle(height: 36, strokeOpacity: 0.18))
|
||||||
|
.accessibilityLabel("Clear app logs")
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
var logsContent = "=== DEVICE INFORMATION ===\n"
|
var logsContent = "=== DEVICE INFORMATION ===\n"
|
||||||
logsContent += "Version: \(UIDevice.current.systemVersion)\n"
|
logsContent += "Version: \(UIDevice.current.systemVersion)\n"
|
||||||
|
|
@ -227,6 +336,80 @@ struct ConsoleLogsView: View {
|
||||||
.accessibilityLabel("Copy app logs")
|
.accessibilityLabel("Copy app logs")
|
||||||
|
|
||||||
exportControl
|
exportControl
|
||||||
|
|
||||||
|
Button {
|
||||||
|
selectedConsoleTab = .syslog
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "rectangle.2.swap")
|
||||||
|
.foregroundColor(accentColor)
|
||||||
|
}
|
||||||
|
.buttonStyle(GlassOvalButtonStyle(height: 36, strokeOpacity: 0.18))
|
||||||
|
.accessibilityLabel("Switch to syslog relay")
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 16)
|
||||||
|
.padding(.vertical, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var syslogFooter: some View {
|
||||||
|
VStack(spacing: 10) {
|
||||||
|
if let error = systemLogStream.lastError {
|
||||||
|
Text(error)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.red)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
HStack(spacing: 6) {
|
||||||
|
Image(systemName: "exclamationmark.triangle.fill")
|
||||||
|
.imageScale(.small)
|
||||||
|
.foregroundColor(.red)
|
||||||
|
Text("\(syslogErrorCount)")
|
||||||
|
.font(.subheadline.weight(.semibold))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(minLength: 8)
|
||||||
|
|
||||||
|
Button(action: toggleSyslogPlayback) {
|
||||||
|
Image(systemName: syslogControlIcon)
|
||||||
|
.foregroundColor(accentColor)
|
||||||
|
}
|
||||||
|
.buttonStyle(GlassOvalButtonStyle(height: 36, strokeOpacity: 0.18))
|
||||||
|
.accessibilityLabel(syslogControlLabel)
|
||||||
|
|
||||||
|
Button(action: { systemLogStream.clear() }) {
|
||||||
|
Image(systemName: "trash")
|
||||||
|
.foregroundColor(accentColor)
|
||||||
|
}
|
||||||
|
.buttonStyle(GlassOvalButtonStyle(height: 36, strokeOpacity: 0.18))
|
||||||
|
.accessibilityLabel("Clear syslog entries")
|
||||||
|
|
||||||
|
Button(action: copySyslogToClipboard) {
|
||||||
|
Image(systemName: "doc.on.doc")
|
||||||
|
.foregroundColor(accentColor)
|
||||||
|
}
|
||||||
|
.buttonStyle(GlassOvalButtonStyle(height: 36, strokeOpacity: 0.18))
|
||||||
|
.accessibilityLabel("Copy syslog entries")
|
||||||
|
|
||||||
|
Button {
|
||||||
|
showingSyslogSpeedSelector.toggle()
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "slider.horizontal.3")
|
||||||
|
.foregroundColor(accentColor)
|
||||||
|
}
|
||||||
|
.buttonStyle(GlassOvalButtonStyle(height: 36, strokeOpacity: 0.18))
|
||||||
|
.accessibilityLabel("Adjust syslog speed")
|
||||||
|
|
||||||
|
Button {
|
||||||
|
selectedConsoleTab = .idevice
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "rectangle.2.swap")
|
||||||
|
.foregroundColor(accentColor)
|
||||||
|
}
|
||||||
|
.buttonStyle(GlassOvalButtonStyle(height: 36, strokeOpacity: 0.18))
|
||||||
|
.accessibilityLabel("Switch to app logs")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
.padding(.vertical, 10)
|
.padding(.vertical, 10)
|
||||||
|
|
@ -288,6 +471,33 @@ struct ConsoleLogsView: View {
|
||||||
|
|
||||||
return fullString
|
return fullString
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func createSyslogAttributedString(_ entry: SystemLogStream.Entry) -> NSAttributedString {
|
||||||
|
let type = logType(for: entry.raw)
|
||||||
|
let fullString = NSMutableAttributedString()
|
||||||
|
|
||||||
|
let timestampString = "[\(DateFormatter.consoleLogsFormatter.string(from: entry.timestamp))]"
|
||||||
|
fullString.append(NSAttributedString(
|
||||||
|
string: timestampString,
|
||||||
|
attributes: [.foregroundColor: colorScheme == .dark ? UIColor.gray : UIColor.darkGray]
|
||||||
|
))
|
||||||
|
fullString.append(NSAttributedString(string: " "))
|
||||||
|
|
||||||
|
let typeAttr = NSAttributedString(
|
||||||
|
string: "[\(type.rawValue)]",
|
||||||
|
attributes: [.foregroundColor: UIColor(colorForLogType(type))]
|
||||||
|
)
|
||||||
|
fullString.append(typeAttr)
|
||||||
|
fullString.append(NSAttributedString(string: " "))
|
||||||
|
|
||||||
|
let messageAttr = NSAttributedString(
|
||||||
|
string: entry.raw,
|
||||||
|
attributes: [.foregroundColor: colorScheme == .dark ? UIColor.white : UIColor.black]
|
||||||
|
)
|
||||||
|
fullString.append(messageAttr)
|
||||||
|
|
||||||
|
return fullString
|
||||||
|
}
|
||||||
private func formatTime(date: Date) -> String {
|
private func formatTime(date: Date) -> String {
|
||||||
let formatter = DateFormatter()
|
let formatter = DateFormatter()
|
||||||
formatter.dateFormat = "HH:mm:ss"
|
formatter.dateFormat = "HH:mm:ss"
|
||||||
|
|
@ -307,6 +517,32 @@ struct ConsoleLogsView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func logType(for line: String) -> LogManager.LogEntry.LogType {
|
||||||
|
let lowercase = line.lowercased()
|
||||||
|
if lowercase.contains("error") {
|
||||||
|
return .error
|
||||||
|
} else if lowercase.contains("warning") {
|
||||||
|
return .warning
|
||||||
|
} else if lowercase.contains("debug") {
|
||||||
|
return .debug
|
||||||
|
} else {
|
||||||
|
return .info
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var syslogErrorCount: Int {
|
||||||
|
systemLogStream.entries.reduce(0) { count, entry in
|
||||||
|
count + (logType(for: entry.raw) == .error ? 1 : 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func intervalLabel(for value: Double) -> String {
|
||||||
|
if value <= 0 {
|
||||||
|
return "Live"
|
||||||
|
}
|
||||||
|
return "\(String(format: "%.1f", value))s"
|
||||||
|
}
|
||||||
|
|
||||||
private func loadIdeviceLogsAsync() async {
|
private func loadIdeviceLogsAsync() async {
|
||||||
guard !isLoadingLogs else { return }
|
guard !isLoadingLogs else { return }
|
||||||
isLoadingLogs = true
|
isLoadingLogs = true
|
||||||
|
|
@ -372,13 +608,15 @@ struct ConsoleLogsView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func startLogCheckTimer() {
|
private func startLogCheckTimer() {
|
||||||
logCheckTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: true) { _ in
|
guard logCheckTimer == nil else { return }
|
||||||
|
logCheckTimer = Timer.scheduledTimer(withTimeInterval: appLogRefreshInterval, repeats: true) { _ in
|
||||||
if isViewActive {
|
if isViewActive {
|
||||||
Task {
|
Task { await checkForNewLogs() }
|
||||||
await checkForNewLogs()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let logCheckTimer {
|
||||||
|
RunLoop.main.add(logCheckTimer, forMode: .common)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func checkForNewLogs() async {
|
private func checkForNewLogs() async {
|
||||||
|
|
@ -439,6 +677,47 @@ struct ConsoleLogsView: View {
|
||||||
logCheckTimer?.invalidate()
|
logCheckTimer?.invalidate()
|
||||||
logCheckTimer = nil
|
logCheckTimer = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func toggleSyslogPlayback() {
|
||||||
|
if !systemLogStream.isStreaming {
|
||||||
|
startHeartbeatInBackground()
|
||||||
|
systemLogStream.start()
|
||||||
|
} else {
|
||||||
|
systemLogStream.togglePause()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func copySyslogToClipboard() {
|
||||||
|
let content = systemLogStream.concatenatedLog()
|
||||||
|
guard !content.isEmpty else {
|
||||||
|
alertTitle = "Export Failed"
|
||||||
|
alertMessage = "No syslog entries to copy."
|
||||||
|
isError = true
|
||||||
|
showingCustomAlert = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
UIPasteboard.general.string = content
|
||||||
|
alertTitle = "Logs Copied"
|
||||||
|
alertMessage = "Latest syslog entries copied to clipboard."
|
||||||
|
isError = false
|
||||||
|
showingCustomAlert = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private var syslogControlIcon: String {
|
||||||
|
if !systemLogStream.isStreaming || systemLogStream.isPaused {
|
||||||
|
return "play.fill"
|
||||||
|
}
|
||||||
|
return "pause.fill"
|
||||||
|
}
|
||||||
|
|
||||||
|
private var syslogControlLabel: String {
|
||||||
|
if !systemLogStream.isStreaming {
|
||||||
|
return "Start syslog relay"
|
||||||
|
}
|
||||||
|
return systemLogStream.isPaused ? "Resume syslog stream" : "Pause syslog stream"
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct GlassOvalButtonStyle: ButtonStyle {
|
private struct GlassOvalButtonStyle: ButtonStyle {
|
||||||
|
|
@ -465,9 +744,22 @@ struct ConsoleLogsView_Previews: PreviewProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private enum ConsoleTab: Hashable {
|
||||||
|
case idevice
|
||||||
|
case syslog
|
||||||
|
}
|
||||||
|
|
||||||
struct ScrollOffsetPreferenceKey: PreferenceKey {
|
struct ScrollOffsetPreferenceKey: PreferenceKey {
|
||||||
static var defaultValue: CGFloat = 0
|
static var defaultValue: CGFloat = 0
|
||||||
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
|
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
|
||||||
value = nextValue()
|
value = nextValue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private extension DateFormatter {
|
||||||
|
static let consoleLogsFormatter: DateFormatter = {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.dateFormat = "HH:mm:ss"
|
||||||
|
return formatter
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,10 @@ struct DisplayView: View {
|
||||||
themeExpansion?.backgroundStyle(for: selectedThemeIdentifier) ?? AppTheme.system.backgroundStyle
|
themeExpansion?.backgroundStyle(for: selectedThemeIdentifier) ?? AppTheme.system.backgroundStyle
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var shouldShowThemeExpansionUpsell: Bool {
|
||||||
|
themeExpansion?.shouldShowThemeExpansionUpsell ?? true
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
ZStack {
|
ZStack {
|
||||||
|
|
@ -121,7 +125,7 @@ struct DisplayView: View {
|
||||||
accentCard
|
accentCard
|
||||||
themeCard
|
themeCard
|
||||||
customThemesSection
|
customThemesSection
|
||||||
} else {
|
} else if shouldShowThemeExpansionUpsell {
|
||||||
// Accent preview remains above
|
// Accent preview remains above
|
||||||
accentPreview
|
accentPreview
|
||||||
|
|
||||||
|
|
@ -132,7 +136,6 @@ struct DisplayView: View {
|
||||||
customThemesPreview
|
customThemesPreview
|
||||||
}
|
}
|
||||||
.zIndex(0)
|
.zIndex(0)
|
||||||
|
|
||||||
themeExpansionUpsellCard
|
themeExpansionUpsellCard
|
||||||
.frame(maxWidth: .infinity) // match other cards’ width
|
.frame(maxWidth: .infinity) // match other cards’ width
|
||||||
.zIndex(1)
|
.zIndex(1)
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,6 @@ struct HomeView: View {
|
||||||
@State private var showPIDSheet = false
|
@State private var showPIDSheet = false
|
||||||
@AppStorage("recentPIDs") private var recentPIDs: [Int] = []
|
@AppStorage("recentPIDs") private var recentPIDs: [Int] = []
|
||||||
@State private var justCopied = false
|
@State private var justCopied = false
|
||||||
@State private var showingConsoleLogsView = false
|
|
||||||
|
|
||||||
@State private var viewDidAppeared = false
|
@State private var viewDidAppeared = false
|
||||||
@State private var pendingJITEnableConfiguration : JITEnableConfiguration? = nil
|
@State private var pendingJITEnableConfiguration : JITEnableConfiguration? = nil
|
||||||
|
|
@ -112,7 +111,6 @@ struct HomeView: View {
|
||||||
if !pinnedLaunchItems.isEmpty {
|
if !pinnedLaunchItems.isEmpty {
|
||||||
launchShortcutsCard
|
launchShortcutsCard
|
||||||
}
|
}
|
||||||
toolsCard
|
|
||||||
tipsCard
|
tipsCard
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 20)
|
.padding(.horizontal, 20)
|
||||||
|
|
@ -243,13 +241,6 @@ struct HomeView: View {
|
||||||
print("Failed to import file: \(error)")
|
print("Failed to import file: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showingConsoleLogsView) {
|
|
||||||
if let manager = themeExpansion {
|
|
||||||
ConsoleLogsView().themeExpansionManager(manager)
|
|
||||||
} else {
|
|
||||||
ConsoleLogsView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.sheet(isPresented: $isShowingInstalledApps) {
|
.sheet(isPresented: $isShowingInstalledApps) {
|
||||||
InstalledAppsListView { selectedBundle in
|
InstalledAppsListView { selectedBundle in
|
||||||
bundleID = selectedBundle
|
bundleID = selectedBundle
|
||||||
|
|
@ -1116,31 +1107,6 @@ struct HomeView: View {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var toolsCard: some View {
|
|
||||||
homeCard {
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
|
||||||
Text("Tools")
|
|
||||||
.font(.headline)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
|
|
||||||
VStack(spacing: 10) {
|
|
||||||
Button {
|
|
||||||
showingConsoleLogsView = true
|
|
||||||
} label: {
|
|
||||||
whiteCardButtonLabel(icon: "terminal", title: "Open Console")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Button {
|
|
||||||
// isShowingInstalledApps = true
|
|
||||||
// } label: {
|
|
||||||
// secondaryButtonLabel(icon: "list.bullet", title: "Installed Apps")
|
|
||||||
// }
|
|
||||||
// .buttonStyle(.plain)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var tipsCard: some View {
|
private var tipsCard: some View {
|
||||||
homeCard {
|
homeCard {
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
|
|
|
||||||
|
|
@ -37,9 +37,13 @@ struct MainTabView: View {
|
||||||
.tabItem { Label("Home", systemImage: "house") }
|
.tabItem { Label("Home", systemImage: "house") }
|
||||||
.tag(0)
|
.tag(0)
|
||||||
|
|
||||||
|
ConsoleLogsView()
|
||||||
|
.tabItem { Label("Console", systemImage: "terminal") }
|
||||||
|
.tag(1)
|
||||||
|
|
||||||
ScriptListView()
|
ScriptListView()
|
||||||
.tabItem { Label("Scripts", systemImage: "scroll") }
|
.tabItem { Label("Scripts", systemImage: "scroll") }
|
||||||
.tag(1)
|
.tag(2)
|
||||||
|
|
||||||
DeviceInfoView()
|
DeviceInfoView()
|
||||||
.tabItem { Label("Device Info", systemImage: "info.circle.fill") }
|
.tabItem { Label("Device Info", systemImage: "info.circle.fill") }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue